├── .dockerignore ├── .env.example ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── story.yml ├── pull_request_template.md └── workflows │ ├── build.yml │ ├── bump.yml │ ├── ci_workflow.yml │ ├── docker.yml │ └── lint.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── docs └── logo.png └── src ├── client.rs ├── commands ├── autopause.rs ├── clear.rs ├── leave.rs ├── manage_sources.rs ├── mod.rs ├── now_playing.rs ├── pause.rs ├── play.rs ├── queue.rs ├── remove.rs ├── repeat.rs ├── resume.rs ├── seek.rs ├── shuffle.rs ├── skip.rs ├── stop.rs ├── summon.rs ├── version.rs └── voteskip.rs ├── connection.rs ├── errors.rs ├── guild ├── cache.rs ├── mod.rs └── settings.rs ├── handlers ├── idle.rs ├── mod.rs ├── serenity.rs └── track_end.rs ├── lib.rs ├── main.rs ├── messaging ├── message.rs ├── messages.rs └── mod.rs ├── sources ├── ffmpeg.rs ├── mod.rs ├── spotify.rs └── youtube.rs ├── test ├── errors.rs ├── mod.rs └── utils.rs └── utils.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | # Docker files 2 | Dockerfile 3 | .dockerignore 4 | 5 | # Git files 6 | .git 7 | .github 8 | .gitignore 9 | LICENSE 10 | README.md 11 | 12 | # Cargo files 13 | **/*.rs.bk 14 | /target/ -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # [REQUIRED] To authenticate with Discord, you must create a Discord app. 2 | # See more: https://discord.com/developers/applications 3 | DISCORD_TOKEN=XXXXXX 4 | DISCORD_APP_ID=XXXXXX 5 | 6 | # [Optional] To support Spotify links, you must create a Spotify app. 7 | # See more: https://developer.spotify.com/dashboard/applications 8 | SPOTIFY_CLIENT_ID=XXXXXX 9 | SPOTIFY_CLIENT_SECRET=XXXXXX -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug report 2 | description: Report errors or unexpected behavior for us to improve. 3 | labels: [🪲 bug, 👓 triage] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: 📝 Description 8 | description: A small description of the bug, if possible provide a set of bullet points to be more precise. 9 | validations: 10 | required: true 11 | 12 | - type: textarea 13 | attributes: 14 | label: 🪜 Reproduction Steps 15 | description: List steps to reproduce the error and details on what happens and what you expected to happen. 16 | value: | 17 | 1. The first step 18 | 2. The second step 19 | 3. ... 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | attributes: 25 | label: ℹ Environment / Computer Info 26 | description: Please provide the details of the system Parrot is running on. 27 | value: | 28 | - **Parrot version**: ` ` 29 | - **Operating System**: ` ` 30 | validations: 31 | required: true 32 | 33 | - type: textarea 34 | attributes: 35 | label: 📸 Screenshots 36 | description: Place any screenshots of the issue here if needed 37 | validations: 38 | required: false 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/story.yml: -------------------------------------------------------------------------------- 1 | name: ✨ User Story 2 | description: Request a new feature or enhancement 3 | labels: [✨ feature, 👓 triage] 4 | body: 5 | - type: textarea 6 | id: rationale 7 | attributes: 8 | label: 🧐 Rationale 9 | description: Why are we implementing this feature and what are the key concepts someone must understand to make sense of it. 10 | validations: 11 | required: true 12 | 13 | - type: textarea 14 | id: description 15 | attributes: 16 | label: 📝 Description 17 | description: What the feature actually is, describes in detail the new functionality. 18 | validations: 19 | required: true 20 | 21 | - type: textarea 22 | id: additional-information 23 | attributes: 24 | label: ➕ Additional Information & References 25 | description: Give us some additional information on the feature request like proposed solutions, links, screenshots, etc. 26 | 27 | - type: markdown 28 | attributes: 29 | value: If you'd like to see this feature implemented, add a 👍 reaction to this post. 30 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | | - | - | 2 | | --- | --- | 3 | | Issue | https://github.com/aquelemiguel/parrot/issues/XXX | 4 | | Dependencies | | 5 | | Decisions | | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Main Workflow 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | build: 8 | name: Build 9 | strategy: 10 | matrix: 11 | rust-version: ['1.65', 'stable'] 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Repository Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Install Toolchain 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: ${{ matrix.rust-version }} 21 | profile: minimal 22 | override: true 23 | 24 | - name: Cache 25 | uses: Swatinem/rust-cache@v1 26 | 27 | - name: Build Binary 28 | run: cargo build --locked 29 | 30 | - name: Build Release Binary 31 | if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') 32 | run: cargo build --release --locked 33 | 34 | - name: Run Unit Tests 35 | run: cargo test 36 | 37 | - name: Run Release Unit Tests 38 | if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') 39 | run: cargo test --release --locked 40 | -------------------------------------------------------------------------------- /.github/workflows/bump.yml: -------------------------------------------------------------------------------- 1 | name: Bump version 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version to bump to (e.g. 1.4.3)' 8 | required: true 9 | type: choice 10 | options: 11 | - major 12 | - minor 13 | - patch 14 | 15 | jobs: 16 | bump: 17 | name: Bump version 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout sources 21 | uses: actions/checkout@v3 22 | 23 | - name: Install Toolchain 24 | uses: actions-rs/toolchain@v1 25 | with: 26 | toolchain: stable 27 | profile: minimal 28 | override: true 29 | 30 | - name: Install cargo-bump 31 | run: cargo install cargo-bump 32 | 33 | - name: Bump Cargo version 34 | run: cargo-bump bump ${{ github.event.inputs.version }} 35 | 36 | - name: Bump Cargo.lock 37 | run: cargo build 38 | 39 | - name: Get latest version 40 | id: get_version 41 | run: echo ::set-output name=VERSION::$(cargo pkgid | cut -d# -f2) 42 | 43 | - name: Create bump PR 44 | uses: peter-evans/create-pull-request@v3 45 | with: 46 | token: ${{ secrets.GITHUB_TOKEN }} 47 | commit-message: 'chore: bump version to ${{ steps.get_version.outputs.VERSION }}' 48 | title: 'chore: bump version to ${{ steps.get_version.outputs.VERSION }}' 49 | body: | 50 | Bump version to ${{ steps.get_version.outputs.VERSION }} 51 | 52 | - [x] Bump version in Cargo.toml 53 | - [x] Bump version in Cargo.lock 54 | committer: GitHub 55 | author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> 56 | branch: bump/${{ steps.get_version.outputs.VERSION }} 57 | base: main 58 | delete-branch: true 59 | labels: ':parrot: release' 60 | -------------------------------------------------------------------------------- /.github/workflows/ci_workflow.yml: -------------------------------------------------------------------------------- 1 | name: CI Workflow 2 | 3 | on: [push, pull_request] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | uses: ./.github/workflows/build.yml 13 | secrets: inherit 14 | 15 | lint: 16 | name: Lint 17 | uses: ./.github/workflows/lint.yml 18 | secrets: inherit 19 | 20 | docker: 21 | name: Docker 22 | needs: [build, lint] 23 | uses: ./.github/workflows/docker.yml 24 | secrets: inherit 25 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Workflow 2 | 3 | on: 4 | push: 5 | tags: ['v*.*.*'] 6 | 7 | workflow_call: 8 | 9 | env: 10 | REGISTRY: ghcr.io 11 | IMAGE_NAME: ${{ github.repository }} 12 | 13 | jobs: 14 | build: 15 | name: Build & Push Docker Image 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | packages: write 20 | steps: 21 | - name: Repository Checkout 22 | uses: actions/checkout@v2 23 | 24 | - name: Registry Login 25 | uses: docker/login-action@v1 26 | with: 27 | registry: ${{ env.REGISTRY }} 28 | username: ${{ github.actor }} 29 | password: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Extract Git Metadata 32 | id: meta 33 | uses: docker/metadata-action@v3 34 | with: 35 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 36 | 37 | - name: Build & Push Docker Image 38 | uses: docker/build-push-action@v3 39 | with: 40 | context: . 41 | push: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }} 42 | tags: ${{ steps.meta.outputs.tags }} 43 | labels: ${{ steps.meta.outputs.labels }} 44 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | clippy: 8 | name: Clippy 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout sources 13 | uses: actions/checkout@v2 14 | 15 | - name: Install stable toolchain 16 | id: toolchain 17 | uses: actions-rs/toolchain@v1 18 | with: 19 | toolchain: stable 20 | components: clippy 21 | profile: minimal 22 | override: true 23 | 24 | - name: Cache 25 | uses: Swatinem/rust-cache@v1 26 | 27 | - name: Run clippy 28 | uses: actions-rs/clippy-check@v1 29 | with: 30 | token: ${{ secrets.GITHUB_TOKEN }} 31 | args: -- -D clippy::all -D warnings 32 | 33 | rustfmt: 34 | name: Format 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | - name: Checkout sources 39 | uses: actions/checkout@v2 40 | 41 | - name: Install stable toolchain 42 | uses: actions-rs/toolchain@v1 43 | with: 44 | toolchain: stable 45 | components: rustfmt 46 | profile: minimal 47 | override: true 48 | 49 | - name: Run cargo fmt 50 | run: cargo fmt --all -- --check 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | 3 | /data/ 4 | 5 | .env 6 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "aead" 13 | version = "0.4.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" 16 | dependencies = [ 17 | "generic-array", 18 | "rand_core", 19 | ] 20 | 21 | [[package]] 22 | name = "aho-corasick" 23 | version = "0.7.18" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 26 | dependencies = [ 27 | "memchr", 28 | ] 29 | 30 | [[package]] 31 | name = "ansi_term" 32 | version = "0.12.1" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 35 | dependencies = [ 36 | "winapi", 37 | ] 38 | 39 | [[package]] 40 | name = "arrayvec" 41 | version = "0.7.2" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" 44 | 45 | [[package]] 46 | name = "async-stream" 47 | version = "0.3.3" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" 50 | dependencies = [ 51 | "async-stream-impl", 52 | "futures-core", 53 | ] 54 | 55 | [[package]] 56 | name = "async-stream-impl" 57 | version = "0.3.3" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" 60 | dependencies = [ 61 | "proc-macro2", 62 | "quote", 63 | "syn 1.0.109", 64 | ] 65 | 66 | [[package]] 67 | name = "async-trait" 68 | version = "0.1.56" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" 71 | dependencies = [ 72 | "proc-macro2", 73 | "quote", 74 | "syn 1.0.109", 75 | ] 76 | 77 | [[package]] 78 | name = "async-tungstenite" 79 | version = "0.17.2" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "a1b71b31561643aa8e7df3effe284fa83ab1a840e52294c5f4bd7bfd8b2becbb" 82 | dependencies = [ 83 | "futures-io", 84 | "futures-util", 85 | "log", 86 | "pin-project-lite", 87 | "tokio", 88 | "tokio-rustls", 89 | "tungstenite", 90 | "webpki-roots", 91 | ] 92 | 93 | [[package]] 94 | name = "audiopus" 95 | version = "0.3.0-rc.0" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "ab55eb0e56d7c6de3d59f544e5db122d7725ec33be6a276ee8241f3be6473955" 98 | dependencies = [ 99 | "audiopus_sys", 100 | ] 101 | 102 | [[package]] 103 | name = "audiopus_sys" 104 | version = "0.2.2" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "62314a1546a2064e033665d658e88c620a62904be945f8147e6b16c3db9f8651" 107 | dependencies = [ 108 | "cmake", 109 | "log", 110 | "pkg-config", 111 | ] 112 | 113 | [[package]] 114 | name = "autocfg" 115 | version = "1.1.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 118 | 119 | [[package]] 120 | name = "base64" 121 | version = "0.13.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" 124 | 125 | [[package]] 126 | name = "base64" 127 | version = "0.21.7" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 130 | 131 | [[package]] 132 | name = "bitflags" 133 | version = "1.3.2" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 136 | 137 | [[package]] 138 | name = "block-buffer" 139 | version = "0.10.2" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" 142 | dependencies = [ 143 | "generic-array", 144 | ] 145 | 146 | [[package]] 147 | name = "bumpalo" 148 | version = "3.10.0" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" 151 | 152 | [[package]] 153 | name = "bytemuck" 154 | version = "1.11.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "a5377c8865e74a160d21f29c2d40669f53286db6eab59b88540cbb12ffc8b835" 157 | 158 | [[package]] 159 | name = "byteorder" 160 | version = "1.4.3" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 163 | 164 | [[package]] 165 | name = "bytes" 166 | version = "1.2.0" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e" 169 | 170 | [[package]] 171 | name = "cc" 172 | version = "1.0.73" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 175 | 176 | [[package]] 177 | name = "cfg-if" 178 | version = "1.0.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 181 | 182 | [[package]] 183 | name = "chrono" 184 | version = "0.4.19" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 187 | dependencies = [ 188 | "libc", 189 | "num-integer", 190 | "num-traits 0.2.15", 191 | "serde", 192 | "time 0.1.44", 193 | "winapi", 194 | ] 195 | 196 | [[package]] 197 | name = "cipher" 198 | version = "0.3.0" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" 201 | dependencies = [ 202 | "generic-array", 203 | ] 204 | 205 | [[package]] 206 | name = "cmake" 207 | version = "0.1.48" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "e8ad8cef104ac57b68b89df3208164d228503abbdce70f6880ffa3d970e7443a" 210 | dependencies = [ 211 | "cc", 212 | ] 213 | 214 | [[package]] 215 | name = "core-foundation" 216 | version = "0.9.3" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 219 | dependencies = [ 220 | "core-foundation-sys", 221 | "libc", 222 | ] 223 | 224 | [[package]] 225 | name = "core-foundation-sys" 226 | version = "0.8.3" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 229 | 230 | [[package]] 231 | name = "cpufeatures" 232 | version = "0.2.2" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" 235 | dependencies = [ 236 | "libc", 237 | ] 238 | 239 | [[package]] 240 | name = "crc32fast" 241 | version = "1.3.2" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 244 | dependencies = [ 245 | "cfg-if", 246 | ] 247 | 248 | [[package]] 249 | name = "crossbeam-utils" 250 | version = "0.8.11" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" 253 | dependencies = [ 254 | "cfg-if", 255 | ] 256 | 257 | [[package]] 258 | name = "crypto-common" 259 | version = "0.1.6" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 262 | dependencies = [ 263 | "generic-array", 264 | "typenum", 265 | ] 266 | 267 | [[package]] 268 | name = "dashmap" 269 | version = "5.3.4" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "3495912c9c1ccf2e18976439f4443f3fee0fd61f424ff99fde6a66b15ecb448f" 272 | dependencies = [ 273 | "cfg-if", 274 | "hashbrown", 275 | "lock_api", 276 | "parking_lot_core", 277 | "serde", 278 | ] 279 | 280 | [[package]] 281 | name = "derivative" 282 | version = "2.2.0" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" 285 | dependencies = [ 286 | "proc-macro2", 287 | "quote", 288 | "syn 1.0.109", 289 | ] 290 | 291 | [[package]] 292 | name = "digest" 293 | version = "0.10.3" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" 296 | dependencies = [ 297 | "block-buffer", 298 | "crypto-common", 299 | ] 300 | 301 | [[package]] 302 | name = "discortp" 303 | version = "0.4.0" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "fb66017646a48220b5ea30d63ac18bb5952f647f1a41ed755880895125d26972" 306 | dependencies = [ 307 | "pnet_macros", 308 | "pnet_macros_support", 309 | ] 310 | 311 | [[package]] 312 | name = "dotenv" 313 | version = "0.15.0" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" 316 | 317 | [[package]] 318 | name = "either" 319 | version = "1.7.0" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" 322 | 323 | [[package]] 324 | name = "encoding_rs" 325 | version = "0.8.31" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" 328 | dependencies = [ 329 | "cfg-if", 330 | ] 331 | 332 | [[package]] 333 | name = "enum_dispatch" 334 | version = "0.3.8" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "0eb359f1476bf611266ac1f5355bc14aeca37b299d0ebccc038ee7058891c9cb" 337 | dependencies = [ 338 | "once_cell", 339 | "proc-macro2", 340 | "quote", 341 | "syn 1.0.109", 342 | ] 343 | 344 | [[package]] 345 | name = "enum_primitive" 346 | version = "0.1.1" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180" 349 | dependencies = [ 350 | "num-traits 0.1.43", 351 | ] 352 | 353 | [[package]] 354 | name = "fastrand" 355 | version = "1.8.0" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" 358 | dependencies = [ 359 | "instant", 360 | ] 361 | 362 | [[package]] 363 | name = "flate2" 364 | version = "1.0.24" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" 367 | dependencies = [ 368 | "crc32fast", 369 | "miniz_oxide", 370 | ] 371 | 372 | [[package]] 373 | name = "flume" 374 | version = "0.10.14" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" 377 | dependencies = [ 378 | "futures-core", 379 | "futures-sink", 380 | "nanorand", 381 | "pin-project", 382 | "spin 0.9.4", 383 | ] 384 | 385 | [[package]] 386 | name = "fnv" 387 | version = "1.0.7" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 390 | 391 | [[package]] 392 | name = "foreign-types" 393 | version = "0.3.2" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 396 | dependencies = [ 397 | "foreign-types-shared", 398 | ] 399 | 400 | [[package]] 401 | name = "foreign-types-shared" 402 | version = "0.1.1" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 405 | 406 | [[package]] 407 | name = "form_urlencoded" 408 | version = "1.1.0" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 411 | dependencies = [ 412 | "percent-encoding", 413 | ] 414 | 415 | [[package]] 416 | name = "futures" 417 | version = "0.3.21" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" 420 | dependencies = [ 421 | "futures-channel", 422 | "futures-core", 423 | "futures-executor", 424 | "futures-io", 425 | "futures-sink", 426 | "futures-task", 427 | "futures-util", 428 | ] 429 | 430 | [[package]] 431 | name = "futures-channel" 432 | version = "0.3.21" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" 435 | dependencies = [ 436 | "futures-core", 437 | "futures-sink", 438 | ] 439 | 440 | [[package]] 441 | name = "futures-core" 442 | version = "0.3.21" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" 445 | 446 | [[package]] 447 | name = "futures-executor" 448 | version = "0.3.21" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" 451 | dependencies = [ 452 | "futures-core", 453 | "futures-task", 454 | "futures-util", 455 | ] 456 | 457 | [[package]] 458 | name = "futures-io" 459 | version = "0.3.21" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" 462 | 463 | [[package]] 464 | name = "futures-macro" 465 | version = "0.3.21" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" 468 | dependencies = [ 469 | "proc-macro2", 470 | "quote", 471 | "syn 1.0.109", 472 | ] 473 | 474 | [[package]] 475 | name = "futures-sink" 476 | version = "0.3.21" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" 479 | 480 | [[package]] 481 | name = "futures-task" 482 | version = "0.3.21" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" 485 | 486 | [[package]] 487 | name = "futures-util" 488 | version = "0.3.21" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" 491 | dependencies = [ 492 | "futures-channel", 493 | "futures-core", 494 | "futures-io", 495 | "futures-macro", 496 | "futures-sink", 497 | "futures-task", 498 | "memchr", 499 | "pin-project-lite", 500 | "pin-utils", 501 | "slab", 502 | ] 503 | 504 | [[package]] 505 | name = "generator" 506 | version = "0.7.1" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "cc184cace1cea8335047a471cc1da80f18acf8a76f3bab2028d499e328948ec7" 509 | dependencies = [ 510 | "cc", 511 | "libc", 512 | "log", 513 | "rustversion", 514 | "windows", 515 | ] 516 | 517 | [[package]] 518 | name = "generic-array" 519 | version = "0.14.5" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" 522 | dependencies = [ 523 | "typenum", 524 | "version_check", 525 | ] 526 | 527 | [[package]] 528 | name = "getrandom" 529 | version = "0.2.7" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" 532 | dependencies = [ 533 | "cfg-if", 534 | "js-sys", 535 | "libc", 536 | "wasi 0.11.0+wasi-snapshot-preview1", 537 | "wasm-bindgen", 538 | ] 539 | 540 | [[package]] 541 | name = "h2" 542 | version = "0.3.13" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" 545 | dependencies = [ 546 | "bytes", 547 | "fnv", 548 | "futures-core", 549 | "futures-sink", 550 | "futures-util", 551 | "http", 552 | "indexmap", 553 | "slab", 554 | "tokio", 555 | "tokio-util", 556 | "tracing", 557 | ] 558 | 559 | [[package]] 560 | name = "hashbrown" 561 | version = "0.12.3" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 564 | 565 | [[package]] 566 | name = "heck" 567 | version = "0.4.1" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 570 | 571 | [[package]] 572 | name = "hermit-abi" 573 | version = "0.1.19" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 576 | dependencies = [ 577 | "libc", 578 | ] 579 | 580 | [[package]] 581 | name = "http" 582 | version = "0.2.8" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" 585 | dependencies = [ 586 | "bytes", 587 | "fnv", 588 | "itoa", 589 | ] 590 | 591 | [[package]] 592 | name = "http-body" 593 | version = "0.4.5" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 596 | dependencies = [ 597 | "bytes", 598 | "http", 599 | "pin-project-lite", 600 | ] 601 | 602 | [[package]] 603 | name = "httparse" 604 | version = "1.7.1" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" 607 | 608 | [[package]] 609 | name = "httpdate" 610 | version = "1.0.2" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 613 | 614 | [[package]] 615 | name = "hyper" 616 | version = "0.14.20" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" 619 | dependencies = [ 620 | "bytes", 621 | "futures-channel", 622 | "futures-core", 623 | "futures-util", 624 | "h2", 625 | "http", 626 | "http-body", 627 | "httparse", 628 | "httpdate", 629 | "itoa", 630 | "pin-project-lite", 631 | "socket2", 632 | "tokio", 633 | "tower-service", 634 | "tracing", 635 | "want", 636 | ] 637 | 638 | [[package]] 639 | name = "hyper-rustls" 640 | version = "0.23.0" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" 643 | dependencies = [ 644 | "http", 645 | "hyper", 646 | "rustls", 647 | "tokio", 648 | "tokio-rustls", 649 | ] 650 | 651 | [[package]] 652 | name = "hyper-tls" 653 | version = "0.5.0" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" 656 | dependencies = [ 657 | "bytes", 658 | "hyper", 659 | "native-tls", 660 | "tokio", 661 | "tokio-native-tls", 662 | ] 663 | 664 | [[package]] 665 | name = "idna" 666 | version = "0.3.0" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 669 | dependencies = [ 670 | "unicode-bidi", 671 | "unicode-normalization", 672 | ] 673 | 674 | [[package]] 675 | name = "indexmap" 676 | version = "1.9.1" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" 679 | dependencies = [ 680 | "autocfg", 681 | "hashbrown", 682 | ] 683 | 684 | [[package]] 685 | name = "instant" 686 | version = "0.1.12" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 689 | dependencies = [ 690 | "cfg-if", 691 | ] 692 | 693 | [[package]] 694 | name = "ipnet" 695 | version = "2.5.0" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" 698 | 699 | [[package]] 700 | name = "itoa" 701 | version = "1.0.2" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" 704 | 705 | [[package]] 706 | name = "js-sys" 707 | version = "0.3.59" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" 710 | dependencies = [ 711 | "wasm-bindgen", 712 | ] 713 | 714 | [[package]] 715 | name = "lazy_static" 716 | version = "1.4.0" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 719 | 720 | [[package]] 721 | name = "libc" 722 | version = "0.2.126" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 725 | 726 | [[package]] 727 | name = "lock_api" 728 | version = "0.4.7" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" 731 | dependencies = [ 732 | "autocfg", 733 | "scopeguard", 734 | ] 735 | 736 | [[package]] 737 | name = "log" 738 | version = "0.4.17" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 741 | dependencies = [ 742 | "cfg-if", 743 | ] 744 | 745 | [[package]] 746 | name = "loom" 747 | version = "0.5.6" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" 750 | dependencies = [ 751 | "cfg-if", 752 | "generator", 753 | "scoped-tls", 754 | "serde", 755 | "serde_json", 756 | "tracing", 757 | "tracing-subscriber", 758 | ] 759 | 760 | [[package]] 761 | name = "matchers" 762 | version = "0.1.0" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 765 | dependencies = [ 766 | "regex-automata", 767 | ] 768 | 769 | [[package]] 770 | name = "maybe-async" 771 | version = "0.2.6" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "6007f9dad048e0a224f27ca599d669fca8cfa0dac804725aab542b2eb032bce6" 774 | dependencies = [ 775 | "proc-macro2", 776 | "quote", 777 | "syn 1.0.109", 778 | ] 779 | 780 | [[package]] 781 | name = "memchr" 782 | version = "2.5.0" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 785 | 786 | [[package]] 787 | name = "mime" 788 | version = "0.3.16" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 791 | 792 | [[package]] 793 | name = "mime_guess" 794 | version = "2.0.4" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" 797 | dependencies = [ 798 | "mime", 799 | "unicase", 800 | ] 801 | 802 | [[package]] 803 | name = "miniz_oxide" 804 | version = "0.5.3" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" 807 | dependencies = [ 808 | "adler", 809 | ] 810 | 811 | [[package]] 812 | name = "mio" 813 | version = "0.8.4" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" 816 | dependencies = [ 817 | "libc", 818 | "log", 819 | "wasi 0.11.0+wasi-snapshot-preview1", 820 | "windows-sys", 821 | ] 822 | 823 | [[package]] 824 | name = "nanorand" 825 | version = "0.7.0" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" 828 | dependencies = [ 829 | "getrandom", 830 | ] 831 | 832 | [[package]] 833 | name = "native-tls" 834 | version = "0.2.10" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" 837 | dependencies = [ 838 | "lazy_static", 839 | "libc", 840 | "log", 841 | "openssl", 842 | "openssl-probe", 843 | "openssl-sys", 844 | "schannel", 845 | "security-framework", 846 | "security-framework-sys", 847 | "tempfile", 848 | ] 849 | 850 | [[package]] 851 | name = "num-integer" 852 | version = "0.1.45" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 855 | dependencies = [ 856 | "autocfg", 857 | "num-traits 0.2.15", 858 | ] 859 | 860 | [[package]] 861 | name = "num-traits" 862 | version = "0.1.43" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" 865 | dependencies = [ 866 | "num-traits 0.2.15", 867 | ] 868 | 869 | [[package]] 870 | name = "num-traits" 871 | version = "0.2.15" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 874 | dependencies = [ 875 | "autocfg", 876 | ] 877 | 878 | [[package]] 879 | name = "num_cpus" 880 | version = "1.13.1" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 883 | dependencies = [ 884 | "hermit-abi", 885 | "libc", 886 | ] 887 | 888 | [[package]] 889 | name = "num_threads" 890 | version = "0.1.6" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" 893 | dependencies = [ 894 | "libc", 895 | ] 896 | 897 | [[package]] 898 | name = "once_cell" 899 | version = "1.13.0" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" 902 | 903 | [[package]] 904 | name = "opaque-debug" 905 | version = "0.3.0" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 908 | 909 | [[package]] 910 | name = "openssl" 911 | version = "0.10.41" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" 914 | dependencies = [ 915 | "bitflags", 916 | "cfg-if", 917 | "foreign-types", 918 | "libc", 919 | "once_cell", 920 | "openssl-macros", 921 | "openssl-sys", 922 | ] 923 | 924 | [[package]] 925 | name = "openssl-macros" 926 | version = "0.1.0" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" 929 | dependencies = [ 930 | "proc-macro2", 931 | "quote", 932 | "syn 1.0.109", 933 | ] 934 | 935 | [[package]] 936 | name = "openssl-probe" 937 | version = "0.1.5" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 940 | 941 | [[package]] 942 | name = "openssl-sys" 943 | version = "0.9.75" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" 946 | dependencies = [ 947 | "autocfg", 948 | "cc", 949 | "libc", 950 | "pkg-config", 951 | "vcpkg", 952 | ] 953 | 954 | [[package]] 955 | name = "ordered-float" 956 | version = "2.10.0" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" 959 | dependencies = [ 960 | "num-traits 0.2.15", 961 | ] 962 | 963 | [[package]] 964 | name = "parking_lot" 965 | version = "0.12.1" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 968 | dependencies = [ 969 | "lock_api", 970 | "parking_lot_core", 971 | ] 972 | 973 | [[package]] 974 | name = "parking_lot_core" 975 | version = "0.9.3" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" 978 | dependencies = [ 979 | "cfg-if", 980 | "libc", 981 | "redox_syscall", 982 | "smallvec", 983 | "windows-sys", 984 | ] 985 | 986 | [[package]] 987 | name = "parrot" 988 | version = "1.6.0" 989 | dependencies = [ 990 | "dotenv", 991 | "lazy_static", 992 | "rand", 993 | "regex", 994 | "rspotify", 995 | "serde", 996 | "serde_json", 997 | "serenity", 998 | "songbird", 999 | "tokio", 1000 | "url", 1001 | ] 1002 | 1003 | [[package]] 1004 | name = "percent-encoding" 1005 | version = "2.2.0" 1006 | source = "registry+https://github.com/rust-lang/crates.io-index" 1007 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 1008 | 1009 | [[package]] 1010 | name = "pin-project" 1011 | version = "1.0.11" 1012 | source = "registry+https://github.com/rust-lang/crates.io-index" 1013 | checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260" 1014 | dependencies = [ 1015 | "pin-project-internal", 1016 | ] 1017 | 1018 | [[package]] 1019 | name = "pin-project-internal" 1020 | version = "1.0.11" 1021 | source = "registry+https://github.com/rust-lang/crates.io-index" 1022 | checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74" 1023 | dependencies = [ 1024 | "proc-macro2", 1025 | "quote", 1026 | "syn 1.0.109", 1027 | ] 1028 | 1029 | [[package]] 1030 | name = "pin-project-lite" 1031 | version = "0.2.9" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 1034 | 1035 | [[package]] 1036 | name = "pin-utils" 1037 | version = "0.1.0" 1038 | source = "registry+https://github.com/rust-lang/crates.io-index" 1039 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1040 | 1041 | [[package]] 1042 | name = "pkg-config" 1043 | version = "0.3.25" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" 1046 | 1047 | [[package]] 1048 | name = "pnet_base" 1049 | version = "0.28.0" 1050 | source = "registry+https://github.com/rust-lang/crates.io-index" 1051 | checksum = "25488cd551a753dcaaa6fffc9f69a7610a412dd8954425bf7ffad5f7d1156fb8" 1052 | 1053 | [[package]] 1054 | name = "pnet_macros" 1055 | version = "0.28.0" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "30490e0852e58402b8fae0d39897b08a24f493023a4d6cf56b2e30f31ed57548" 1058 | dependencies = [ 1059 | "proc-macro2", 1060 | "quote", 1061 | "regex", 1062 | "syn 1.0.109", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "pnet_macros_support" 1067 | version = "0.28.0" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "d4714e10f30cab023005adce048f2d30dd4ac4f093662abf2220855655ef8f90" 1070 | dependencies = [ 1071 | "pnet_base", 1072 | ] 1073 | 1074 | [[package]] 1075 | name = "poly1305" 1076 | version = "0.7.2" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" 1079 | dependencies = [ 1080 | "cpufeatures", 1081 | "opaque-debug", 1082 | "universal-hash", 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "ppv-lite86" 1087 | version = "0.2.16" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" 1090 | 1091 | [[package]] 1092 | name = "proc-macro2" 1093 | version = "1.0.78" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 1096 | dependencies = [ 1097 | "unicode-ident", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "quote" 1102 | version = "1.0.35" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 1105 | dependencies = [ 1106 | "proc-macro2", 1107 | ] 1108 | 1109 | [[package]] 1110 | name = "rand" 1111 | version = "0.8.5" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1114 | dependencies = [ 1115 | "libc", 1116 | "rand_chacha", 1117 | "rand_core", 1118 | ] 1119 | 1120 | [[package]] 1121 | name = "rand_chacha" 1122 | version = "0.3.1" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1125 | dependencies = [ 1126 | "ppv-lite86", 1127 | "rand_core", 1128 | ] 1129 | 1130 | [[package]] 1131 | name = "rand_core" 1132 | version = "0.6.3" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 1135 | dependencies = [ 1136 | "getrandom", 1137 | ] 1138 | 1139 | [[package]] 1140 | name = "redox_syscall" 1141 | version = "0.2.16" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 1144 | dependencies = [ 1145 | "bitflags", 1146 | ] 1147 | 1148 | [[package]] 1149 | name = "regex" 1150 | version = "1.6.0" 1151 | source = "registry+https://github.com/rust-lang/crates.io-index" 1152 | checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" 1153 | dependencies = [ 1154 | "aho-corasick", 1155 | "memchr", 1156 | "regex-syntax", 1157 | ] 1158 | 1159 | [[package]] 1160 | name = "regex-automata" 1161 | version = "0.1.10" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1164 | dependencies = [ 1165 | "regex-syntax", 1166 | ] 1167 | 1168 | [[package]] 1169 | name = "regex-syntax" 1170 | version = "0.6.27" 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" 1172 | checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" 1173 | 1174 | [[package]] 1175 | name = "remove_dir_all" 1176 | version = "0.5.3" 1177 | source = "registry+https://github.com/rust-lang/crates.io-index" 1178 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 1179 | dependencies = [ 1180 | "winapi", 1181 | ] 1182 | 1183 | [[package]] 1184 | name = "reqwest" 1185 | version = "0.11.11" 1186 | source = "registry+https://github.com/rust-lang/crates.io-index" 1187 | checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" 1188 | dependencies = [ 1189 | "base64 0.13.0", 1190 | "bytes", 1191 | "encoding_rs", 1192 | "futures-core", 1193 | "futures-util", 1194 | "h2", 1195 | "http", 1196 | "http-body", 1197 | "hyper", 1198 | "hyper-rustls", 1199 | "hyper-tls", 1200 | "ipnet", 1201 | "js-sys", 1202 | "lazy_static", 1203 | "log", 1204 | "mime", 1205 | "mime_guess", 1206 | "native-tls", 1207 | "percent-encoding", 1208 | "pin-project-lite", 1209 | "rustls", 1210 | "rustls-pemfile", 1211 | "serde", 1212 | "serde_json", 1213 | "serde_urlencoded", 1214 | "tokio", 1215 | "tokio-native-tls", 1216 | "tokio-rustls", 1217 | "tokio-socks", 1218 | "tokio-util", 1219 | "tower-service", 1220 | "url", 1221 | "wasm-bindgen", 1222 | "wasm-bindgen-futures", 1223 | "web-sys", 1224 | "webpki-roots", 1225 | "winreg", 1226 | ] 1227 | 1228 | [[package]] 1229 | name = "ring" 1230 | version = "0.16.20" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" 1233 | dependencies = [ 1234 | "cc", 1235 | "libc", 1236 | "once_cell", 1237 | "spin 0.5.2", 1238 | "untrusted", 1239 | "web-sys", 1240 | "winapi", 1241 | ] 1242 | 1243 | [[package]] 1244 | name = "rspotify" 1245 | version = "0.12.0" 1246 | source = "registry+https://github.com/rust-lang/crates.io-index" 1247 | checksum = "87c6f1d86b10201655f0cd4002088bafe4abcc75cc610613c995abd719f40fcb" 1248 | dependencies = [ 1249 | "async-stream", 1250 | "async-trait", 1251 | "base64 0.21.7", 1252 | "chrono", 1253 | "futures", 1254 | "getrandom", 1255 | "log", 1256 | "maybe-async", 1257 | "rspotify-http", 1258 | "rspotify-macros", 1259 | "rspotify-model", 1260 | "serde", 1261 | "serde_json", 1262 | "sha2", 1263 | "thiserror", 1264 | "url", 1265 | ] 1266 | 1267 | [[package]] 1268 | name = "rspotify-http" 1269 | version = "0.12.0" 1270 | source = "registry+https://github.com/rust-lang/crates.io-index" 1271 | checksum = "dad45cd393a8685ee36ec6d2accbb2c955e21ac036a2e4eb175985783f30ed78" 1272 | dependencies = [ 1273 | "async-trait", 1274 | "log", 1275 | "maybe-async", 1276 | "reqwest", 1277 | "serde_json", 1278 | "thiserror", 1279 | ] 1280 | 1281 | [[package]] 1282 | name = "rspotify-macros" 1283 | version = "0.12.0" 1284 | source = "registry+https://github.com/rust-lang/crates.io-index" 1285 | checksum = "cc4892882851a97ee7210e423725ce116e8239157c649af37e208fe93855638a" 1286 | 1287 | [[package]] 1288 | name = "rspotify-model" 1289 | version = "0.12.0" 1290 | source = "registry+https://github.com/rust-lang/crates.io-index" 1291 | checksum = "bae90ab3d6e4cb4ccd7f2887c4363d19b1419800e132d3fb95e2f9b24c05f4d7" 1292 | dependencies = [ 1293 | "chrono", 1294 | "enum_dispatch", 1295 | "serde", 1296 | "serde_json", 1297 | "strum", 1298 | "thiserror", 1299 | ] 1300 | 1301 | [[package]] 1302 | name = "rustls" 1303 | version = "0.20.6" 1304 | source = "registry+https://github.com/rust-lang/crates.io-index" 1305 | checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" 1306 | dependencies = [ 1307 | "log", 1308 | "ring", 1309 | "sct", 1310 | "webpki", 1311 | ] 1312 | 1313 | [[package]] 1314 | name = "rustls-pemfile" 1315 | version = "1.0.0" 1316 | source = "registry+https://github.com/rust-lang/crates.io-index" 1317 | checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9" 1318 | dependencies = [ 1319 | "base64 0.13.0", 1320 | ] 1321 | 1322 | [[package]] 1323 | name = "rustversion" 1324 | version = "1.0.8" 1325 | source = "registry+https://github.com/rust-lang/crates.io-index" 1326 | checksum = "24c8ad4f0c00e1eb5bc7614d236a7f1300e3dbd76b68cac8e06fb00b015ad8d8" 1327 | 1328 | [[package]] 1329 | name = "ryu" 1330 | version = "1.0.10" 1331 | source = "registry+https://github.com/rust-lang/crates.io-index" 1332 | checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" 1333 | 1334 | [[package]] 1335 | name = "salsa20" 1336 | version = "0.9.0" 1337 | source = "registry+https://github.com/rust-lang/crates.io-index" 1338 | checksum = "0c0fbb5f676da676c260ba276a8f43a8dc67cf02d1438423aeb1c677a7212686" 1339 | dependencies = [ 1340 | "cipher", 1341 | "zeroize", 1342 | ] 1343 | 1344 | [[package]] 1345 | name = "schannel" 1346 | version = "0.1.20" 1347 | source = "registry+https://github.com/rust-lang/crates.io-index" 1348 | checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" 1349 | dependencies = [ 1350 | "lazy_static", 1351 | "windows-sys", 1352 | ] 1353 | 1354 | [[package]] 1355 | name = "scoped-tls" 1356 | version = "1.0.0" 1357 | source = "registry+https://github.com/rust-lang/crates.io-index" 1358 | checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" 1359 | 1360 | [[package]] 1361 | name = "scopeguard" 1362 | version = "1.1.0" 1363 | source = "registry+https://github.com/rust-lang/crates.io-index" 1364 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1365 | 1366 | [[package]] 1367 | name = "sct" 1368 | version = "0.7.0" 1369 | source = "registry+https://github.com/rust-lang/crates.io-index" 1370 | checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" 1371 | dependencies = [ 1372 | "ring", 1373 | "untrusted", 1374 | ] 1375 | 1376 | [[package]] 1377 | name = "security-framework" 1378 | version = "2.6.1" 1379 | source = "registry+https://github.com/rust-lang/crates.io-index" 1380 | checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" 1381 | dependencies = [ 1382 | "bitflags", 1383 | "core-foundation", 1384 | "core-foundation-sys", 1385 | "libc", 1386 | "security-framework-sys", 1387 | ] 1388 | 1389 | [[package]] 1390 | name = "security-framework-sys" 1391 | version = "2.6.1" 1392 | source = "registry+https://github.com/rust-lang/crates.io-index" 1393 | checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" 1394 | dependencies = [ 1395 | "core-foundation-sys", 1396 | "libc", 1397 | ] 1398 | 1399 | [[package]] 1400 | name = "serde" 1401 | version = "1.0.152" 1402 | source = "registry+https://github.com/rust-lang/crates.io-index" 1403 | checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" 1404 | dependencies = [ 1405 | "serde_derive", 1406 | ] 1407 | 1408 | [[package]] 1409 | name = "serde-value" 1410 | version = "0.7.0" 1411 | source = "registry+https://github.com/rust-lang/crates.io-index" 1412 | checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" 1413 | dependencies = [ 1414 | "ordered-float", 1415 | "serde", 1416 | ] 1417 | 1418 | [[package]] 1419 | name = "serde_derive" 1420 | version = "1.0.152" 1421 | source = "registry+https://github.com/rust-lang/crates.io-index" 1422 | checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" 1423 | dependencies = [ 1424 | "proc-macro2", 1425 | "quote", 1426 | "syn 1.0.109", 1427 | ] 1428 | 1429 | [[package]] 1430 | name = "serde_json" 1431 | version = "1.0.82" 1432 | source = "registry+https://github.com/rust-lang/crates.io-index" 1433 | checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" 1434 | dependencies = [ 1435 | "itoa", 1436 | "ryu", 1437 | "serde", 1438 | ] 1439 | 1440 | [[package]] 1441 | name = "serde_repr" 1442 | version = "0.1.8" 1443 | source = "registry+https://github.com/rust-lang/crates.io-index" 1444 | checksum = "a2ad84e47328a31223de7fed7a4f5087f2d6ddfe586cf3ca25b7a165bc0a5aed" 1445 | dependencies = [ 1446 | "proc-macro2", 1447 | "quote", 1448 | "syn 1.0.109", 1449 | ] 1450 | 1451 | [[package]] 1452 | name = "serde_urlencoded" 1453 | version = "0.7.1" 1454 | source = "registry+https://github.com/rust-lang/crates.io-index" 1455 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1456 | dependencies = [ 1457 | "form_urlencoded", 1458 | "itoa", 1459 | "ryu", 1460 | "serde", 1461 | ] 1462 | 1463 | [[package]] 1464 | name = "serenity" 1465 | version = "0.11.5" 1466 | source = "registry+https://github.com/rust-lang/crates.io-index" 1467 | checksum = "82fd5e7b5858ad96e99d440138f34f5b98e1b959ebcd3a1036203b30e78eb788" 1468 | dependencies = [ 1469 | "async-trait", 1470 | "async-tungstenite", 1471 | "base64 0.13.0", 1472 | "bitflags", 1473 | "bytes", 1474 | "cfg-if", 1475 | "dashmap", 1476 | "flate2", 1477 | "futures", 1478 | "mime", 1479 | "mime_guess", 1480 | "parking_lot", 1481 | "percent-encoding", 1482 | "reqwest", 1483 | "rustversion", 1484 | "serde", 1485 | "serde-value", 1486 | "serde_json", 1487 | "time 0.3.11", 1488 | "tokio", 1489 | "tracing", 1490 | "typemap_rev", 1491 | "url", 1492 | ] 1493 | 1494 | [[package]] 1495 | name = "serenity-voice-model" 1496 | version = "0.1.1" 1497 | source = "registry+https://github.com/rust-lang/crates.io-index" 1498 | checksum = "8be3aec8849ca2fde1e8a5dfbed96fbd68e9b5f4283fbe277d8694ce811d4952" 1499 | dependencies = [ 1500 | "bitflags", 1501 | "enum_primitive", 1502 | "serde", 1503 | "serde_json", 1504 | "serde_repr", 1505 | ] 1506 | 1507 | [[package]] 1508 | name = "sha-1" 1509 | version = "0.10.0" 1510 | source = "registry+https://github.com/rust-lang/crates.io-index" 1511 | checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" 1512 | dependencies = [ 1513 | "cfg-if", 1514 | "cpufeatures", 1515 | "digest", 1516 | ] 1517 | 1518 | [[package]] 1519 | name = "sha2" 1520 | version = "0.10.2" 1521 | source = "registry+https://github.com/rust-lang/crates.io-index" 1522 | checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" 1523 | dependencies = [ 1524 | "cfg-if", 1525 | "cpufeatures", 1526 | "digest", 1527 | ] 1528 | 1529 | [[package]] 1530 | name = "sharded-slab" 1531 | version = "0.1.4" 1532 | source = "registry+https://github.com/rust-lang/crates.io-index" 1533 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 1534 | dependencies = [ 1535 | "lazy_static", 1536 | ] 1537 | 1538 | [[package]] 1539 | name = "signal-hook-registry" 1540 | version = "1.4.0" 1541 | source = "registry+https://github.com/rust-lang/crates.io-index" 1542 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 1543 | dependencies = [ 1544 | "libc", 1545 | ] 1546 | 1547 | [[package]] 1548 | name = "slab" 1549 | version = "0.4.7" 1550 | source = "registry+https://github.com/rust-lang/crates.io-index" 1551 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" 1552 | dependencies = [ 1553 | "autocfg", 1554 | ] 1555 | 1556 | [[package]] 1557 | name = "smallvec" 1558 | version = "1.9.0" 1559 | source = "registry+https://github.com/rust-lang/crates.io-index" 1560 | checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" 1561 | 1562 | [[package]] 1563 | name = "socket2" 1564 | version = "0.4.4" 1565 | source = "registry+https://github.com/rust-lang/crates.io-index" 1566 | checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" 1567 | dependencies = [ 1568 | "libc", 1569 | "winapi", 1570 | ] 1571 | 1572 | [[package]] 1573 | name = "songbird" 1574 | version = "0.3.2" 1575 | source = "registry+https://github.com/rust-lang/crates.io-index" 1576 | checksum = "32f686a0fd771939de1da3e43cee45169fafe1595770b94680572cf18bdef288" 1577 | dependencies = [ 1578 | "async-trait", 1579 | "async-tungstenite", 1580 | "audiopus", 1581 | "byteorder", 1582 | "dashmap", 1583 | "derivative", 1584 | "discortp", 1585 | "flume", 1586 | "futures", 1587 | "parking_lot", 1588 | "pin-project", 1589 | "rand", 1590 | "serde", 1591 | "serde_json", 1592 | "serenity", 1593 | "serenity-voice-model", 1594 | "streamcatcher", 1595 | "symphonia-core", 1596 | "tokio", 1597 | "tracing", 1598 | "tracing-futures", 1599 | "typemap_rev", 1600 | "url", 1601 | "uuid", 1602 | "xsalsa20poly1305", 1603 | ] 1604 | 1605 | [[package]] 1606 | name = "spin" 1607 | version = "0.5.2" 1608 | source = "registry+https://github.com/rust-lang/crates.io-index" 1609 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 1610 | 1611 | [[package]] 1612 | name = "spin" 1613 | version = "0.9.4" 1614 | source = "registry+https://github.com/rust-lang/crates.io-index" 1615 | checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" 1616 | dependencies = [ 1617 | "lock_api", 1618 | ] 1619 | 1620 | [[package]] 1621 | name = "streamcatcher" 1622 | version = "1.0.1" 1623 | source = "registry+https://github.com/rust-lang/crates.io-index" 1624 | checksum = "71664755c349abb0758fda6218fb2d2391ca2a73f9302c03b145491db4fcea29" 1625 | dependencies = [ 1626 | "crossbeam-utils", 1627 | "futures-util", 1628 | "loom", 1629 | ] 1630 | 1631 | [[package]] 1632 | name = "strum" 1633 | version = "0.25.0" 1634 | source = "registry+https://github.com/rust-lang/crates.io-index" 1635 | checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" 1636 | dependencies = [ 1637 | "strum_macros", 1638 | ] 1639 | 1640 | [[package]] 1641 | name = "strum_macros" 1642 | version = "0.25.3" 1643 | source = "registry+https://github.com/rust-lang/crates.io-index" 1644 | checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" 1645 | dependencies = [ 1646 | "heck", 1647 | "proc-macro2", 1648 | "quote", 1649 | "rustversion", 1650 | "syn 2.0.51", 1651 | ] 1652 | 1653 | [[package]] 1654 | name = "subtle" 1655 | version = "2.4.1" 1656 | source = "registry+https://github.com/rust-lang/crates.io-index" 1657 | checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" 1658 | 1659 | [[package]] 1660 | name = "symphonia-core" 1661 | version = "0.5.1" 1662 | source = "registry+https://github.com/rust-lang/crates.io-index" 1663 | checksum = "199a6417cd4115bac79289b64b859358ea050b7add0ceb364dc991f628c5b347" 1664 | dependencies = [ 1665 | "arrayvec", 1666 | "bitflags", 1667 | "bytemuck", 1668 | "lazy_static", 1669 | "log", 1670 | ] 1671 | 1672 | [[package]] 1673 | name = "syn" 1674 | version = "1.0.109" 1675 | source = "registry+https://github.com/rust-lang/crates.io-index" 1676 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1677 | dependencies = [ 1678 | "proc-macro2", 1679 | "quote", 1680 | "unicode-ident", 1681 | ] 1682 | 1683 | [[package]] 1684 | name = "syn" 1685 | version = "2.0.51" 1686 | source = "registry+https://github.com/rust-lang/crates.io-index" 1687 | checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c" 1688 | dependencies = [ 1689 | "proc-macro2", 1690 | "quote", 1691 | "unicode-ident", 1692 | ] 1693 | 1694 | [[package]] 1695 | name = "tempfile" 1696 | version = "3.3.0" 1697 | source = "registry+https://github.com/rust-lang/crates.io-index" 1698 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 1699 | dependencies = [ 1700 | "cfg-if", 1701 | "fastrand", 1702 | "libc", 1703 | "redox_syscall", 1704 | "remove_dir_all", 1705 | "winapi", 1706 | ] 1707 | 1708 | [[package]] 1709 | name = "thiserror" 1710 | version = "1.0.31" 1711 | source = "registry+https://github.com/rust-lang/crates.io-index" 1712 | checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" 1713 | dependencies = [ 1714 | "thiserror-impl", 1715 | ] 1716 | 1717 | [[package]] 1718 | name = "thiserror-impl" 1719 | version = "1.0.31" 1720 | source = "registry+https://github.com/rust-lang/crates.io-index" 1721 | checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" 1722 | dependencies = [ 1723 | "proc-macro2", 1724 | "quote", 1725 | "syn 1.0.109", 1726 | ] 1727 | 1728 | [[package]] 1729 | name = "thread_local" 1730 | version = "1.1.4" 1731 | source = "registry+https://github.com/rust-lang/crates.io-index" 1732 | checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" 1733 | dependencies = [ 1734 | "once_cell", 1735 | ] 1736 | 1737 | [[package]] 1738 | name = "time" 1739 | version = "0.1.44" 1740 | source = "registry+https://github.com/rust-lang/crates.io-index" 1741 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 1742 | dependencies = [ 1743 | "libc", 1744 | "wasi 0.10.0+wasi-snapshot-preview1", 1745 | "winapi", 1746 | ] 1747 | 1748 | [[package]] 1749 | name = "time" 1750 | version = "0.3.11" 1751 | source = "registry+https://github.com/rust-lang/crates.io-index" 1752 | checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" 1753 | dependencies = [ 1754 | "itoa", 1755 | "libc", 1756 | "num_threads", 1757 | "serde", 1758 | ] 1759 | 1760 | [[package]] 1761 | name = "tinyvec" 1762 | version = "1.6.0" 1763 | source = "registry+https://github.com/rust-lang/crates.io-index" 1764 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1765 | dependencies = [ 1766 | "tinyvec_macros", 1767 | ] 1768 | 1769 | [[package]] 1770 | name = "tinyvec_macros" 1771 | version = "0.1.0" 1772 | source = "registry+https://github.com/rust-lang/crates.io-index" 1773 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 1774 | 1775 | [[package]] 1776 | name = "tokio" 1777 | version = "1.20.1" 1778 | source = "registry+https://github.com/rust-lang/crates.io-index" 1779 | checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" 1780 | dependencies = [ 1781 | "autocfg", 1782 | "bytes", 1783 | "libc", 1784 | "memchr", 1785 | "mio", 1786 | "num_cpus", 1787 | "once_cell", 1788 | "pin-project-lite", 1789 | "signal-hook-registry", 1790 | "socket2", 1791 | "tokio-macros", 1792 | "winapi", 1793 | ] 1794 | 1795 | [[package]] 1796 | name = "tokio-macros" 1797 | version = "1.8.0" 1798 | source = "registry+https://github.com/rust-lang/crates.io-index" 1799 | checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" 1800 | dependencies = [ 1801 | "proc-macro2", 1802 | "quote", 1803 | "syn 1.0.109", 1804 | ] 1805 | 1806 | [[package]] 1807 | name = "tokio-native-tls" 1808 | version = "0.3.0" 1809 | source = "registry+https://github.com/rust-lang/crates.io-index" 1810 | checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" 1811 | dependencies = [ 1812 | "native-tls", 1813 | "tokio", 1814 | ] 1815 | 1816 | [[package]] 1817 | name = "tokio-rustls" 1818 | version = "0.23.4" 1819 | source = "registry+https://github.com/rust-lang/crates.io-index" 1820 | checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" 1821 | dependencies = [ 1822 | "rustls", 1823 | "tokio", 1824 | "webpki", 1825 | ] 1826 | 1827 | [[package]] 1828 | name = "tokio-socks" 1829 | version = "0.5.1" 1830 | source = "registry+https://github.com/rust-lang/crates.io-index" 1831 | checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" 1832 | dependencies = [ 1833 | "either", 1834 | "futures-util", 1835 | "thiserror", 1836 | "tokio", 1837 | ] 1838 | 1839 | [[package]] 1840 | name = "tokio-util" 1841 | version = "0.7.3" 1842 | source = "registry+https://github.com/rust-lang/crates.io-index" 1843 | checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" 1844 | dependencies = [ 1845 | "bytes", 1846 | "futures-core", 1847 | "futures-sink", 1848 | "pin-project-lite", 1849 | "tokio", 1850 | "tracing", 1851 | ] 1852 | 1853 | [[package]] 1854 | name = "tower-service" 1855 | version = "0.3.2" 1856 | source = "registry+https://github.com/rust-lang/crates.io-index" 1857 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1858 | 1859 | [[package]] 1860 | name = "tracing" 1861 | version = "0.1.35" 1862 | source = "registry+https://github.com/rust-lang/crates.io-index" 1863 | checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" 1864 | dependencies = [ 1865 | "cfg-if", 1866 | "log", 1867 | "pin-project-lite", 1868 | "tracing-attributes", 1869 | "tracing-core", 1870 | ] 1871 | 1872 | [[package]] 1873 | name = "tracing-attributes" 1874 | version = "0.1.22" 1875 | source = "registry+https://github.com/rust-lang/crates.io-index" 1876 | checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" 1877 | dependencies = [ 1878 | "proc-macro2", 1879 | "quote", 1880 | "syn 1.0.109", 1881 | ] 1882 | 1883 | [[package]] 1884 | name = "tracing-core" 1885 | version = "0.1.28" 1886 | source = "registry+https://github.com/rust-lang/crates.io-index" 1887 | checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" 1888 | dependencies = [ 1889 | "once_cell", 1890 | "valuable", 1891 | ] 1892 | 1893 | [[package]] 1894 | name = "tracing-futures" 1895 | version = "0.2.5" 1896 | source = "registry+https://github.com/rust-lang/crates.io-index" 1897 | checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" 1898 | dependencies = [ 1899 | "pin-project", 1900 | "tracing", 1901 | ] 1902 | 1903 | [[package]] 1904 | name = "tracing-log" 1905 | version = "0.1.3" 1906 | source = "registry+https://github.com/rust-lang/crates.io-index" 1907 | checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" 1908 | dependencies = [ 1909 | "lazy_static", 1910 | "log", 1911 | "tracing-core", 1912 | ] 1913 | 1914 | [[package]] 1915 | name = "tracing-subscriber" 1916 | version = "0.3.15" 1917 | source = "registry+https://github.com/rust-lang/crates.io-index" 1918 | checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" 1919 | dependencies = [ 1920 | "ansi_term", 1921 | "matchers", 1922 | "once_cell", 1923 | "regex", 1924 | "sharded-slab", 1925 | "smallvec", 1926 | "thread_local", 1927 | "tracing", 1928 | "tracing-core", 1929 | "tracing-log", 1930 | ] 1931 | 1932 | [[package]] 1933 | name = "try-lock" 1934 | version = "0.2.3" 1935 | source = "registry+https://github.com/rust-lang/crates.io-index" 1936 | checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" 1937 | 1938 | [[package]] 1939 | name = "tungstenite" 1940 | version = "0.17.3" 1941 | source = "registry+https://github.com/rust-lang/crates.io-index" 1942 | checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" 1943 | dependencies = [ 1944 | "base64 0.13.0", 1945 | "byteorder", 1946 | "bytes", 1947 | "http", 1948 | "httparse", 1949 | "log", 1950 | "rand", 1951 | "rustls", 1952 | "sha-1", 1953 | "thiserror", 1954 | "url", 1955 | "utf-8", 1956 | "webpki", 1957 | ] 1958 | 1959 | [[package]] 1960 | name = "typemap_rev" 1961 | version = "0.1.5" 1962 | source = "registry+https://github.com/rust-lang/crates.io-index" 1963 | checksum = "ed5b74f0a24b5454580a79abb6994393b09adf0ab8070f15827cb666255de155" 1964 | 1965 | [[package]] 1966 | name = "typenum" 1967 | version = "1.15.0" 1968 | source = "registry+https://github.com/rust-lang/crates.io-index" 1969 | checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" 1970 | 1971 | [[package]] 1972 | name = "unicase" 1973 | version = "2.6.0" 1974 | source = "registry+https://github.com/rust-lang/crates.io-index" 1975 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 1976 | dependencies = [ 1977 | "version_check", 1978 | ] 1979 | 1980 | [[package]] 1981 | name = "unicode-bidi" 1982 | version = "0.3.8" 1983 | source = "registry+https://github.com/rust-lang/crates.io-index" 1984 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 1985 | 1986 | [[package]] 1987 | name = "unicode-ident" 1988 | version = "1.0.2" 1989 | source = "registry+https://github.com/rust-lang/crates.io-index" 1990 | checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" 1991 | 1992 | [[package]] 1993 | name = "unicode-normalization" 1994 | version = "0.1.21" 1995 | source = "registry+https://github.com/rust-lang/crates.io-index" 1996 | checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" 1997 | dependencies = [ 1998 | "tinyvec", 1999 | ] 2000 | 2001 | [[package]] 2002 | name = "universal-hash" 2003 | version = "0.4.1" 2004 | source = "registry+https://github.com/rust-lang/crates.io-index" 2005 | checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" 2006 | dependencies = [ 2007 | "generic-array", 2008 | "subtle", 2009 | ] 2010 | 2011 | [[package]] 2012 | name = "untrusted" 2013 | version = "0.7.1" 2014 | source = "registry+https://github.com/rust-lang/crates.io-index" 2015 | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" 2016 | 2017 | [[package]] 2018 | name = "url" 2019 | version = "2.3.1" 2020 | source = "registry+https://github.com/rust-lang/crates.io-index" 2021 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 2022 | dependencies = [ 2023 | "form_urlencoded", 2024 | "idna", 2025 | "percent-encoding", 2026 | "serde", 2027 | ] 2028 | 2029 | [[package]] 2030 | name = "utf-8" 2031 | version = "0.7.6" 2032 | source = "registry+https://github.com/rust-lang/crates.io-index" 2033 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 2034 | 2035 | [[package]] 2036 | name = "uuid" 2037 | version = "0.8.2" 2038 | source = "registry+https://github.com/rust-lang/crates.io-index" 2039 | checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" 2040 | dependencies = [ 2041 | "getrandom", 2042 | ] 2043 | 2044 | [[package]] 2045 | name = "valuable" 2046 | version = "0.1.0" 2047 | source = "registry+https://github.com/rust-lang/crates.io-index" 2048 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 2049 | 2050 | [[package]] 2051 | name = "vcpkg" 2052 | version = "0.2.15" 2053 | source = "registry+https://github.com/rust-lang/crates.io-index" 2054 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2055 | 2056 | [[package]] 2057 | name = "version_check" 2058 | version = "0.9.4" 2059 | source = "registry+https://github.com/rust-lang/crates.io-index" 2060 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 2061 | 2062 | [[package]] 2063 | name = "want" 2064 | version = "0.3.0" 2065 | source = "registry+https://github.com/rust-lang/crates.io-index" 2066 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 2067 | dependencies = [ 2068 | "log", 2069 | "try-lock", 2070 | ] 2071 | 2072 | [[package]] 2073 | name = "wasi" 2074 | version = "0.10.0+wasi-snapshot-preview1" 2075 | source = "registry+https://github.com/rust-lang/crates.io-index" 2076 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 2077 | 2078 | [[package]] 2079 | name = "wasi" 2080 | version = "0.11.0+wasi-snapshot-preview1" 2081 | source = "registry+https://github.com/rust-lang/crates.io-index" 2082 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 2083 | 2084 | [[package]] 2085 | name = "wasm-bindgen" 2086 | version = "0.2.82" 2087 | source = "registry+https://github.com/rust-lang/crates.io-index" 2088 | checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" 2089 | dependencies = [ 2090 | "cfg-if", 2091 | "wasm-bindgen-macro", 2092 | ] 2093 | 2094 | [[package]] 2095 | name = "wasm-bindgen-backend" 2096 | version = "0.2.82" 2097 | source = "registry+https://github.com/rust-lang/crates.io-index" 2098 | checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" 2099 | dependencies = [ 2100 | "bumpalo", 2101 | "log", 2102 | "once_cell", 2103 | "proc-macro2", 2104 | "quote", 2105 | "syn 1.0.109", 2106 | "wasm-bindgen-shared", 2107 | ] 2108 | 2109 | [[package]] 2110 | name = "wasm-bindgen-futures" 2111 | version = "0.4.32" 2112 | source = "registry+https://github.com/rust-lang/crates.io-index" 2113 | checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" 2114 | dependencies = [ 2115 | "cfg-if", 2116 | "js-sys", 2117 | "wasm-bindgen", 2118 | "web-sys", 2119 | ] 2120 | 2121 | [[package]] 2122 | name = "wasm-bindgen-macro" 2123 | version = "0.2.82" 2124 | source = "registry+https://github.com/rust-lang/crates.io-index" 2125 | checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" 2126 | dependencies = [ 2127 | "quote", 2128 | "wasm-bindgen-macro-support", 2129 | ] 2130 | 2131 | [[package]] 2132 | name = "wasm-bindgen-macro-support" 2133 | version = "0.2.82" 2134 | source = "registry+https://github.com/rust-lang/crates.io-index" 2135 | checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" 2136 | dependencies = [ 2137 | "proc-macro2", 2138 | "quote", 2139 | "syn 1.0.109", 2140 | "wasm-bindgen-backend", 2141 | "wasm-bindgen-shared", 2142 | ] 2143 | 2144 | [[package]] 2145 | name = "wasm-bindgen-shared" 2146 | version = "0.2.82" 2147 | source = "registry+https://github.com/rust-lang/crates.io-index" 2148 | checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" 2149 | 2150 | [[package]] 2151 | name = "web-sys" 2152 | version = "0.3.59" 2153 | source = "registry+https://github.com/rust-lang/crates.io-index" 2154 | checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" 2155 | dependencies = [ 2156 | "js-sys", 2157 | "wasm-bindgen", 2158 | ] 2159 | 2160 | [[package]] 2161 | name = "webpki" 2162 | version = "0.22.0" 2163 | source = "registry+https://github.com/rust-lang/crates.io-index" 2164 | checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" 2165 | dependencies = [ 2166 | "ring", 2167 | "untrusted", 2168 | ] 2169 | 2170 | [[package]] 2171 | name = "webpki-roots" 2172 | version = "0.22.4" 2173 | source = "registry+https://github.com/rust-lang/crates.io-index" 2174 | checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" 2175 | dependencies = [ 2176 | "webpki", 2177 | ] 2178 | 2179 | [[package]] 2180 | name = "winapi" 2181 | version = "0.3.9" 2182 | source = "registry+https://github.com/rust-lang/crates.io-index" 2183 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2184 | dependencies = [ 2185 | "winapi-i686-pc-windows-gnu", 2186 | "winapi-x86_64-pc-windows-gnu", 2187 | ] 2188 | 2189 | [[package]] 2190 | name = "winapi-i686-pc-windows-gnu" 2191 | version = "0.4.0" 2192 | source = "registry+https://github.com/rust-lang/crates.io-index" 2193 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2194 | 2195 | [[package]] 2196 | name = "winapi-x86_64-pc-windows-gnu" 2197 | version = "0.4.0" 2198 | source = "registry+https://github.com/rust-lang/crates.io-index" 2199 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2200 | 2201 | [[package]] 2202 | name = "windows" 2203 | version = "0.32.0" 2204 | source = "registry+https://github.com/rust-lang/crates.io-index" 2205 | checksum = "fbedf6db9096bc2364adce0ae0aa636dcd89f3c3f2cd67947062aaf0ca2a10ec" 2206 | dependencies = [ 2207 | "windows_aarch64_msvc 0.32.0", 2208 | "windows_i686_gnu 0.32.0", 2209 | "windows_i686_msvc 0.32.0", 2210 | "windows_x86_64_gnu 0.32.0", 2211 | "windows_x86_64_msvc 0.32.0", 2212 | ] 2213 | 2214 | [[package]] 2215 | name = "windows-sys" 2216 | version = "0.36.1" 2217 | source = "registry+https://github.com/rust-lang/crates.io-index" 2218 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 2219 | dependencies = [ 2220 | "windows_aarch64_msvc 0.36.1", 2221 | "windows_i686_gnu 0.36.1", 2222 | "windows_i686_msvc 0.36.1", 2223 | "windows_x86_64_gnu 0.36.1", 2224 | "windows_x86_64_msvc 0.36.1", 2225 | ] 2226 | 2227 | [[package]] 2228 | name = "windows_aarch64_msvc" 2229 | version = "0.32.0" 2230 | source = "registry+https://github.com/rust-lang/crates.io-index" 2231 | checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" 2232 | 2233 | [[package]] 2234 | name = "windows_aarch64_msvc" 2235 | version = "0.36.1" 2236 | source = "registry+https://github.com/rust-lang/crates.io-index" 2237 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 2238 | 2239 | [[package]] 2240 | name = "windows_i686_gnu" 2241 | version = "0.32.0" 2242 | source = "registry+https://github.com/rust-lang/crates.io-index" 2243 | checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" 2244 | 2245 | [[package]] 2246 | name = "windows_i686_gnu" 2247 | version = "0.36.1" 2248 | source = "registry+https://github.com/rust-lang/crates.io-index" 2249 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 2250 | 2251 | [[package]] 2252 | name = "windows_i686_msvc" 2253 | version = "0.32.0" 2254 | source = "registry+https://github.com/rust-lang/crates.io-index" 2255 | checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" 2256 | 2257 | [[package]] 2258 | name = "windows_i686_msvc" 2259 | version = "0.36.1" 2260 | source = "registry+https://github.com/rust-lang/crates.io-index" 2261 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 2262 | 2263 | [[package]] 2264 | name = "windows_x86_64_gnu" 2265 | version = "0.32.0" 2266 | source = "registry+https://github.com/rust-lang/crates.io-index" 2267 | checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" 2268 | 2269 | [[package]] 2270 | name = "windows_x86_64_gnu" 2271 | version = "0.36.1" 2272 | source = "registry+https://github.com/rust-lang/crates.io-index" 2273 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 2274 | 2275 | [[package]] 2276 | name = "windows_x86_64_msvc" 2277 | version = "0.32.0" 2278 | source = "registry+https://github.com/rust-lang/crates.io-index" 2279 | checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" 2280 | 2281 | [[package]] 2282 | name = "windows_x86_64_msvc" 2283 | version = "0.36.1" 2284 | source = "registry+https://github.com/rust-lang/crates.io-index" 2285 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 2286 | 2287 | [[package]] 2288 | name = "winreg" 2289 | version = "0.10.1" 2290 | source = "registry+https://github.com/rust-lang/crates.io-index" 2291 | checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" 2292 | dependencies = [ 2293 | "winapi", 2294 | ] 2295 | 2296 | [[package]] 2297 | name = "xsalsa20poly1305" 2298 | version = "0.8.0" 2299 | source = "registry+https://github.com/rust-lang/crates.io-index" 2300 | checksum = "e68bcb965d6c650091450b95cea12f07dcd299a01c15e2f9433b0813ea3c0886" 2301 | dependencies = [ 2302 | "aead", 2303 | "poly1305", 2304 | "rand_core", 2305 | "salsa20", 2306 | "subtle", 2307 | "zeroize", 2308 | ] 2309 | 2310 | [[package]] 2311 | name = "zeroize" 2312 | version = "1.3.0" 2313 | source = "registry+https://github.com/rust-lang/crates.io-index" 2314 | checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" 2315 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "parrot" 3 | version = "1.6.0" 4 | authors = ["aquelemiguel"] 5 | edition = "2018" 6 | description = "A Discord music bot built in Rust" 7 | repository = "https://github.com/aquelemiguel/parrot" 8 | license = "MIT" 9 | keywords = ["discord", "music-bot", "rust"] 10 | 11 | [dependencies] 12 | dotenv = "0.15.0" 13 | lazy_static = "1.4.0" 14 | rand = "0.8.5" 15 | regex = "1.5.5" 16 | rspotify = "0.12.0" 17 | serde_json = "1.0.79" 18 | url = "2.3.1" 19 | serde = "1.0.152" 20 | 21 | [dependencies.songbird] 22 | version = "0.3.2" 23 | features = ["builtin-queue", "yt-dlp"] 24 | 25 | [dependencies.serenity] 26 | version = "0.11.5" 27 | default-features = false 28 | features = [ 29 | "cache", 30 | "collector", 31 | "client", 32 | "gateway", 33 | "model", 34 | "rustls_backend", 35 | "unstable_discord_api", 36 | "voice", 37 | ] 38 | 39 | [dependencies.tokio] 40 | version = "1.17.0" 41 | features = ["macros", "rt-multi-thread"] 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build image 2 | # Necessary dependencies to build Parrot 3 | FROM rust:slim-bullseye as build 4 | 5 | RUN apt-get update && apt-get install -y \ 6 | build-essential autoconf automake cmake libtool libssl-dev pkg-config 7 | 8 | WORKDIR "/parrot" 9 | 10 | # Cache cargo build dependencies by creating a dummy source 11 | RUN mkdir src 12 | RUN echo "fn main() {}" > src/main.rs 13 | COPY Cargo.toml ./ 14 | COPY Cargo.lock ./ 15 | RUN cargo build --release --locked 16 | 17 | COPY . . 18 | RUN cargo build --release --locked 19 | 20 | # Release image 21 | # Necessary dependencies to run Parrot 22 | FROM debian:bullseye-slim 23 | 24 | RUN apt-get update && apt-get install -y python3-pip ffmpeg 25 | RUN pip install -U yt-dlp 26 | 27 | COPY --from=build /parrot/target/release/parrot . 28 | 29 | CMD ["./parrot"] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Miguel Mano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Light 3 |

4 | 5 |

6 | A hassle-free, highly performant, host-it-yourself Discord music bot 7 |

8 | 9 |

10 | 11 | 12 | 13 | 14 |

15 | 16 | ## Deployment 17 | 18 | ### Usage 19 | 20 | Just [create a bot account](https://github.com/aquelemiguel/parrot/wiki/Create-Your-Discord-Bot), and copy its **token** and **application id** to a `.env` with the `DISCORD_TOKEN` and `DISCORD_APP_ID` environment variables respectively. Optionally, you may also define `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET`. We recommend using our [.env.example](https://github.com/aquelemiguel/parrot/blob/main/.env.example) as a starting point. 21 | 22 | ### Docker 23 | 24 | ```shell 25 | docker run -d --env-file .env --restart unless-stopped --name parrot ghcr.io/aquelemiguel/parrot:latest 26 | ``` 27 | 28 | ## Development 29 | 30 | Make sure you've installed Rust. You can install Rust and its package manager, `cargo` by following the instructions on https://rustup.rs/. 31 | After installing the requirements below, simply run `cargo run`. 32 | 33 | ### Linux/MacOS 34 | 35 | The commands below install a C compiler, GNU autotools and FFmpeg, as well as [yt-dlp](https://github.com/yt-dlp/yt-dlp) through Python's package manager, pip. 36 | 37 | #### Linux 38 | 39 | ```shell 40 | apt install build-essential autoconf automake libtool ffmpeg 41 | pip install -U yt-dlp 42 | ``` 43 | 44 | #### MacOS 45 | 46 | ```shell 47 | brew install autoconf automake libtool ffmpeg 48 | pip install -U yt-dlp 49 | ``` 50 | 51 | ### Windows 52 | 53 | If you are using the MSVC toolchain, a prebuilt DLL for Opus is already provided for you. 54 | You will only need to download [FFmpeg](https://ffmpeg.org/download.html), and install [yt-dlp](https://github.com/yt-dlp/yt-dlp) which can be done through Python's package manager, pip. 55 | 56 | ```shell 57 | pip install -U yt-dlp 58 | ``` 59 | 60 | If you are using Windows Subsystem for Linux (WSL), you should follow the [Linux/MacOS](#linuxmacos) guide, and, in addition to the other required packages, install pkg-config, which you may do by running: 61 | 62 | ```shell 63 | apt install pkg-config 64 | ``` 65 | 66 | ## Testing 67 | 68 | Tests are available inside the `src/tests` folder. They can be run via `cargo test`. It's recommended that you run the tests before submitting your Pull Request. 69 | Increasing the test coverage is also welcome. 70 | 71 | ### Docker 72 | 73 | Within the project folder, simply run the following: 74 | 75 | ```shell 76 | docker build -t parrot . 77 | docker run -d --env-file .env parrot 78 | ``` 79 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aquelemiguel/parrot/a6c1e88a1e360d46a91bc536985db87af72245b3/docs/logo.png -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use serenity::model::gateway::GatewayIntents; 2 | use songbird::serenity::SerenityInit; 3 | 4 | use std::{collections::HashMap, env, error::Error}; 5 | 6 | use crate::{ 7 | guild::{cache::GuildCacheMap, settings::GuildSettingsMap}, 8 | handlers::SerenityHandler, 9 | }; 10 | 11 | pub struct Client { 12 | client: serenity::Client, 13 | } 14 | 15 | impl Client { 16 | pub async fn default() -> Result> { 17 | let token = env::var("DISCORD_TOKEN").expect("Fatality! DISCORD_TOKEN not set!"); 18 | Client::new(token).await 19 | } 20 | 21 | pub async fn new(token: String) -> Result> { 22 | let application_id = env::var("DISCORD_APP_ID") 23 | .expect("Fatality! DISCORD_APP_ID not set!") 24 | .parse()?; 25 | 26 | let gateway_intents = GatewayIntents::non_privileged(); 27 | 28 | let client = serenity::Client::builder(token, gateway_intents) 29 | .event_handler(SerenityHandler) 30 | .application_id(application_id) 31 | .register_songbird() 32 | .await?; 33 | 34 | let mut data = client.data.write().await; 35 | data.insert::(HashMap::default()); 36 | data.insert::(HashMap::default()); 37 | drop(data); 38 | 39 | Ok(Client { client }) 40 | } 41 | 42 | pub async fn start(&mut self) -> Result<(), serenity::Error> { 43 | self.client.start().await 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/commands/autopause.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::ParrotError, 3 | guild::settings::{GuildSettings, GuildSettingsMap}, 4 | messaging::message::ParrotMessage, 5 | utils::create_response, 6 | }; 7 | use serenity::{ 8 | client::Context, 9 | model::application::interaction::application_command::ApplicationCommandInteraction, 10 | }; 11 | 12 | pub async fn autopause( 13 | ctx: &Context, 14 | interaction: &mut ApplicationCommandInteraction, 15 | ) -> Result<(), ParrotError> { 16 | let guild_id = interaction.guild_id.unwrap(); 17 | let mut data = ctx.data.write().await; 18 | let settings = data.get_mut::().unwrap(); 19 | 20 | let guild_settings = settings 21 | .entry(guild_id) 22 | .or_insert_with(|| GuildSettings::new(guild_id)); 23 | guild_settings.toggle_autopause(); 24 | guild_settings.save()?; 25 | 26 | if guild_settings.autopause { 27 | create_response(&ctx.http, interaction, ParrotMessage::AutopauseOn).await 28 | } else { 29 | create_response(&ctx.http, interaction, ParrotMessage::AutopauseOff).await 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/commands/clear.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::{verify, ParrotError}, 3 | handlers::track_end::update_queue_messages, 4 | messaging::message::ParrotMessage, 5 | utils::create_response, 6 | }; 7 | use serenity::{ 8 | client::Context, 9 | model::application::interaction::application_command::ApplicationCommandInteraction, 10 | }; 11 | 12 | pub async fn clear( 13 | ctx: &Context, 14 | interaction: &mut ApplicationCommandInteraction, 15 | ) -> Result<(), ParrotError> { 16 | let guild_id = interaction.guild_id.unwrap(); 17 | let manager = songbird::get(ctx).await.unwrap(); 18 | let call = manager.get(guild_id).unwrap(); 19 | 20 | let handler = call.lock().await; 21 | let queue = handler.queue().current_queue(); 22 | 23 | verify(queue.len() > 1, ParrotError::QueueEmpty)?; 24 | 25 | handler.queue().modify_queue(|v| { 26 | v.drain(1..); 27 | }); 28 | 29 | // refetch the queue after modification 30 | let queue = handler.queue().current_queue(); 31 | drop(handler); 32 | 33 | create_response(&ctx.http, interaction, ParrotMessage::Clear).await?; 34 | update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await; 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/leave.rs: -------------------------------------------------------------------------------- 1 | use crate::{errors::ParrotError, messaging::message::ParrotMessage, utils::create_response}; 2 | use serenity::{ 3 | client::Context, 4 | model::application::interaction::application_command::ApplicationCommandInteraction, 5 | }; 6 | 7 | pub async fn leave( 8 | ctx: &Context, 9 | interaction: &mut ApplicationCommandInteraction, 10 | ) -> Result<(), ParrotError> { 11 | let guild_id = interaction.guild_id.unwrap(); 12 | let manager = songbird::get(ctx).await.unwrap(); 13 | manager.remove(guild_id).await.unwrap(); 14 | 15 | create_response(&ctx.http, interaction, ParrotMessage::Leaving).await 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/manage_sources.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::ParrotError, 3 | guild::settings::{GuildSettings, GuildSettingsMap}, 4 | messaging::messages::{ 5 | DOMAIN_FORM_ALLOWED_PLACEHOLDER, DOMAIN_FORM_ALLOWED_TITLE, DOMAIN_FORM_BANNED_PLACEHOLDER, 6 | DOMAIN_FORM_BANNED_TITLE, DOMAIN_FORM_TITLE, 7 | }, 8 | }; 9 | use serenity::{ 10 | builder::{CreateComponents, CreateInputText}, 11 | client::Context, 12 | collector::ModalInteractionCollectorBuilder, 13 | futures::StreamExt, 14 | model::{ 15 | application::interaction::application_command::ApplicationCommandInteraction, 16 | prelude::{ 17 | component::{ActionRowComponent, InputTextStyle}, 18 | interaction::InteractionResponseType, 19 | }, 20 | }, 21 | }; 22 | 23 | pub async fn allow( 24 | ctx: &Context, 25 | interaction: &mut ApplicationCommandInteraction, 26 | ) -> Result<(), ParrotError> { 27 | let guild_id = interaction.guild_id.unwrap(); 28 | 29 | let mut data = ctx.data.write().await; 30 | let settings = data.get_mut::().unwrap(); 31 | 32 | let guild_settings = settings 33 | .entry(guild_id) 34 | .or_insert_with(|| GuildSettings::new(guild_id)); 35 | 36 | // transform the domain sets from the settings into a string 37 | let allowed_str = guild_settings 38 | .allowed_domains 39 | .clone() 40 | .into_iter() 41 | .collect::>() 42 | .join(";"); 43 | 44 | let banned_str = guild_settings 45 | .banned_domains 46 | .clone() 47 | .into_iter() 48 | .collect::>() 49 | .join(";"); 50 | 51 | drop(data); 52 | 53 | let mut allowed_input = CreateInputText::default(); 54 | allowed_input 55 | .label(DOMAIN_FORM_ALLOWED_TITLE) 56 | .custom_id("allowed_domains") 57 | .style(InputTextStyle::Paragraph) 58 | .placeholder(DOMAIN_FORM_ALLOWED_PLACEHOLDER) 59 | .value(allowed_str) 60 | .required(false); 61 | 62 | let mut banned_input = CreateInputText::default(); 63 | banned_input 64 | .label(DOMAIN_FORM_BANNED_TITLE) 65 | .custom_id("banned_domains") 66 | .style(InputTextStyle::Paragraph) 67 | .placeholder(DOMAIN_FORM_BANNED_PLACEHOLDER) 68 | .value(banned_str) 69 | .required(false); 70 | 71 | let mut components = CreateComponents::default(); 72 | components 73 | .create_action_row(|r| r.add_input_text(allowed_input)) 74 | .create_action_row(|r| r.add_input_text(banned_input)); 75 | 76 | interaction 77 | .create_interaction_response(&ctx.http, |r| { 78 | r.kind(InteractionResponseType::Modal); 79 | r.interaction_response_data(|d| { 80 | d.title(DOMAIN_FORM_TITLE); 81 | d.custom_id("manage_domains"); 82 | d.set_components(components) 83 | }) 84 | }) 85 | .await?; 86 | 87 | // collect the submitted data 88 | let collector = ModalInteractionCollectorBuilder::new(ctx) 89 | .filter(|int| int.data.custom_id == "manage_domains") 90 | .build(); 91 | 92 | collector 93 | .then(|int| async move { 94 | let mut data = ctx.data.write().await; 95 | let settings = data.get_mut::().unwrap(); 96 | 97 | let inputs: Vec<_> = int 98 | .data 99 | .components 100 | .iter() 101 | .flat_map(|r| r.components.iter()) 102 | .collect(); 103 | 104 | let guild_settings = settings.get_mut(&guild_id).unwrap(); 105 | 106 | for input in inputs.iter() { 107 | if let ActionRowComponent::InputText(it) = input { 108 | if it.custom_id == "allowed_domains" { 109 | guild_settings.set_allowed_domains(&it.value); 110 | } 111 | 112 | if it.custom_id == "banned_domains" { 113 | guild_settings.set_banned_domains(&it.value); 114 | } 115 | } 116 | } 117 | 118 | guild_settings.update_domains(); 119 | guild_settings.save().unwrap(); 120 | 121 | // it's now safe to close the modal, so send a response to it 122 | int.create_interaction_response(&ctx.http, |r| { 123 | r.kind(InteractionResponseType::DeferredUpdateMessage) 124 | }) 125 | .await 126 | .ok(); 127 | }) 128 | .collect::>() 129 | .await; 130 | 131 | Ok(()) 132 | } 133 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod autopause; 2 | pub mod clear; 3 | pub mod leave; 4 | pub mod manage_sources; 5 | pub mod now_playing; 6 | pub mod pause; 7 | pub mod play; 8 | pub mod queue; 9 | pub mod remove; 10 | pub mod repeat; 11 | pub mod resume; 12 | pub mod seek; 13 | pub mod shuffle; 14 | pub mod skip; 15 | pub mod stop; 16 | pub mod summon; 17 | pub mod version; 18 | pub mod voteskip; 19 | -------------------------------------------------------------------------------- /src/commands/now_playing.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::ParrotError, 3 | utils::{create_embed_response, create_now_playing_embed}, 4 | }; 5 | use serenity::{ 6 | client::Context, 7 | model::application::interaction::application_command::ApplicationCommandInteraction, 8 | }; 9 | 10 | pub async fn now_playing( 11 | ctx: &Context, 12 | interaction: &mut ApplicationCommandInteraction, 13 | ) -> Result<(), ParrotError> { 14 | let guild_id = interaction.guild_id.unwrap(); 15 | let manager = songbird::get(ctx).await.unwrap(); 16 | let call = manager.get(guild_id).unwrap(); 17 | 18 | let handler = call.lock().await; 19 | let track = handler 20 | .queue() 21 | .current() 22 | .ok_or(ParrotError::NothingPlaying)?; 23 | 24 | let embed = create_now_playing_embed(&track).await; 25 | create_embed_response(&ctx.http, interaction, embed).await 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/pause.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::{verify, ParrotError}, 3 | messaging::message::ParrotMessage, 4 | utils::create_response, 5 | }; 6 | use serenity::{ 7 | client::Context, 8 | model::application::interaction::application_command::ApplicationCommandInteraction, 9 | }; 10 | 11 | pub async fn pause( 12 | ctx: &Context, 13 | interaction: &mut ApplicationCommandInteraction, 14 | ) -> Result<(), ParrotError> { 15 | let guild_id = interaction.guild_id.unwrap(); 16 | let manager = songbird::get(ctx).await.unwrap(); 17 | let call = manager.get(guild_id).unwrap(); 18 | 19 | let handler = call.lock().await; 20 | let queue = handler.queue(); 21 | 22 | verify(!queue.is_empty(), ParrotError::NothingPlaying)?; 23 | verify(queue.pause(), ParrotError::Other("Failed to pause"))?; 24 | 25 | create_response(&ctx.http, interaction, ParrotMessage::Pause).await 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/play.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | commands::{skip::force_skip_top_track, summon::summon}, 3 | errors::{verify, ParrotError}, 4 | guild::settings::{GuildSettings, GuildSettingsMap}, 5 | handlers::track_end::update_queue_messages, 6 | messaging::message::ParrotMessage, 7 | messaging::messages::{ 8 | PLAY_QUEUE, PLAY_TOP, SPOTIFY_AUTH_FAILED, TRACK_DURATION, TRACK_TIME_TO_PLAY, 9 | }, 10 | sources::{ 11 | spotify::{Spotify, SPOTIFY}, 12 | youtube::{YouTube, YouTubeRestartable}, 13 | }, 14 | utils::{ 15 | compare_domains, create_now_playing_embed, create_response, edit_embed_response, 16 | edit_response, get_human_readable_timestamp, 17 | }, 18 | }; 19 | use serenity::{ 20 | builder::CreateEmbed, client::Context, 21 | model::application::interaction::application_command::ApplicationCommandInteraction, 22 | prelude::Mutex, 23 | }; 24 | use songbird::{input::Restartable, tracks::TrackHandle, Call}; 25 | use std::{cmp::Ordering, error::Error as StdError, sync::Arc, time::Duration}; 26 | use url::Url; 27 | 28 | #[derive(Clone, Copy)] 29 | pub enum Mode { 30 | End, 31 | Next, 32 | All, 33 | Reverse, 34 | Shuffle, 35 | Jump, 36 | } 37 | 38 | #[derive(Clone)] 39 | pub enum QueryType { 40 | Keywords(String), 41 | KeywordList(Vec), 42 | VideoLink(String), 43 | PlaylistLink(String), 44 | } 45 | 46 | pub async fn play( 47 | ctx: &Context, 48 | interaction: &mut ApplicationCommandInteraction, 49 | ) -> Result<(), ParrotError> { 50 | let args = interaction.data.options.clone(); 51 | let first_arg = args.first().unwrap(); 52 | 53 | let mode = match first_arg.name.as_str() { 54 | "next" => Mode::Next, 55 | "all" => Mode::All, 56 | "reverse" => Mode::Reverse, 57 | "shuffle" => Mode::Shuffle, 58 | "jump" => Mode::Jump, 59 | _ => Mode::End, 60 | }; 61 | 62 | let url = match mode { 63 | Mode::End => first_arg.value.as_ref().unwrap().as_str().unwrap(), 64 | _ => first_arg 65 | .options 66 | .first() 67 | .unwrap() 68 | .value 69 | .as_ref() 70 | .unwrap() 71 | .as_str() 72 | .unwrap(), 73 | }; 74 | 75 | let guild_id = interaction.guild_id.unwrap(); 76 | let manager = songbird::get(ctx).await.unwrap(); 77 | 78 | // try to join a voice channel if not in one just yet 79 | summon(ctx, interaction, false).await?; 80 | let call = manager.get(guild_id).unwrap(); 81 | 82 | // determine whether this is a link or a query string 83 | let query_type = match Url::parse(url) { 84 | Ok(url_data) => match url_data.host_str() { 85 | Some("open.spotify.com") => { 86 | let spotify = SPOTIFY.lock().await; 87 | let spotify = verify(spotify.as_ref(), ParrotError::Other(SPOTIFY_AUTH_FAILED))?; 88 | Some(Spotify::extract(spotify, url).await?) 89 | } 90 | Some(other) => { 91 | let mut data = ctx.data.write().await; 92 | let settings = data.get_mut::().unwrap(); 93 | let guild_settings = settings 94 | .entry(guild_id) 95 | .or_insert_with(|| GuildSettings::new(guild_id)); 96 | 97 | let is_allowed = guild_settings 98 | .allowed_domains 99 | .iter() 100 | .any(|d| compare_domains(d, other)); 101 | 102 | let is_banned = guild_settings 103 | .banned_domains 104 | .iter() 105 | .any(|d| compare_domains(d, other)); 106 | 107 | if is_banned || (guild_settings.banned_domains.is_empty() && !is_allowed) { 108 | return create_response( 109 | &ctx.http, 110 | interaction, 111 | ParrotMessage::PlayDomainBanned { 112 | domain: other.to_string(), 113 | }, 114 | ) 115 | .await; 116 | } 117 | 118 | YouTube::extract(url) 119 | } 120 | None => None, 121 | }, 122 | Err(_) => { 123 | let mut data = ctx.data.write().await; 124 | let settings = data.get_mut::().unwrap(); 125 | let guild_settings = settings 126 | .entry(guild_id) 127 | .or_insert_with(|| GuildSettings::new(guild_id)); 128 | 129 | if guild_settings.banned_domains.contains("youtube.com") 130 | || (guild_settings.banned_domains.is_empty() 131 | && !guild_settings.allowed_domains.contains("youtube.com")) 132 | { 133 | return create_response( 134 | &ctx.http, 135 | interaction, 136 | ParrotMessage::PlayDomainBanned { 137 | domain: "youtube.com".to_string(), 138 | }, 139 | ) 140 | .await; 141 | } 142 | 143 | Some(QueryType::Keywords(url.to_string())) 144 | } 145 | }; 146 | 147 | let query_type = verify( 148 | query_type, 149 | ParrotError::Other("Something went wrong while parsing your query!"), 150 | )?; 151 | 152 | // reply with a temporary message while we fetch the source 153 | // needed because interactions must be replied within 3s and queueing takes longer 154 | create_response(&ctx.http, interaction, ParrotMessage::Search).await?; 155 | 156 | let handler = call.lock().await; 157 | let queue_was_empty = handler.queue().is_empty(); 158 | drop(handler); 159 | 160 | match mode { 161 | Mode::End => match query_type.clone() { 162 | QueryType::Keywords(_) | QueryType::VideoLink(_) => { 163 | let queue = enqueue_track(&call, &query_type).await?; 164 | update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await; 165 | } 166 | QueryType::PlaylistLink(url) => { 167 | let urls = YouTubeRestartable::ytdl_playlist(&url, mode) 168 | .await 169 | .ok_or(ParrotError::Other("failed to fetch playlist"))?; 170 | 171 | for url in urls.iter() { 172 | let Ok(queue) = 173 | enqueue_track(&call, &QueryType::VideoLink(url.to_string())).await 174 | else { 175 | continue; 176 | }; 177 | update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await; 178 | } 179 | } 180 | QueryType::KeywordList(keywords_list) => { 181 | for keywords in keywords_list.iter() { 182 | let queue = 183 | enqueue_track(&call, &QueryType::Keywords(keywords.to_string())).await?; 184 | update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await; 185 | } 186 | } 187 | }, 188 | Mode::Next => match query_type.clone() { 189 | QueryType::Keywords(_) | QueryType::VideoLink(_) => { 190 | let queue = insert_track(&call, &query_type, 1).await?; 191 | update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await; 192 | } 193 | QueryType::PlaylistLink(url) => { 194 | let urls = YouTubeRestartable::ytdl_playlist(&url, mode) 195 | .await 196 | .ok_or(ParrotError::Other("failed to fetch playlist"))?; 197 | 198 | for (idx, url) in urls.into_iter().enumerate() { 199 | let Ok(queue) = insert_track(&call, &QueryType::VideoLink(url), idx + 1).await 200 | else { 201 | continue; 202 | }; 203 | update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await; 204 | } 205 | } 206 | QueryType::KeywordList(keywords_list) => { 207 | for (idx, keywords) in keywords_list.into_iter().enumerate() { 208 | let queue = 209 | insert_track(&call, &QueryType::Keywords(keywords), idx + 1).await?; 210 | update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await; 211 | } 212 | } 213 | }, 214 | Mode::Jump => match query_type.clone() { 215 | QueryType::Keywords(_) | QueryType::VideoLink(_) => { 216 | let mut queue = enqueue_track(&call, &query_type).await?; 217 | 218 | if !queue_was_empty { 219 | rotate_tracks(&call, 1).await.ok(); 220 | queue = force_skip_top_track(&call.lock().await).await?; 221 | } 222 | 223 | update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await; 224 | } 225 | QueryType::PlaylistLink(url) => { 226 | let urls = YouTubeRestartable::ytdl_playlist(&url, mode) 227 | .await 228 | .ok_or(ParrotError::Other("failed to fetch playlist"))?; 229 | 230 | let mut insert_idx = 1; 231 | 232 | for (i, url) in urls.into_iter().enumerate() { 233 | let Ok(mut queue) = 234 | insert_track(&call, &QueryType::VideoLink(url), insert_idx).await 235 | else { 236 | continue; 237 | }; 238 | 239 | if i == 0 && !queue_was_empty { 240 | queue = force_skip_top_track(&call.lock().await).await?; 241 | } else { 242 | insert_idx += 1; 243 | } 244 | 245 | update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await; 246 | } 247 | } 248 | QueryType::KeywordList(keywords_list) => { 249 | let mut insert_idx = 1; 250 | 251 | for (i, keywords) in keywords_list.into_iter().enumerate() { 252 | let mut queue = 253 | insert_track(&call, &QueryType::Keywords(keywords), insert_idx).await?; 254 | 255 | if i == 0 && !queue_was_empty { 256 | queue = force_skip_top_track(&call.lock().await).await?; 257 | } else { 258 | insert_idx += 1; 259 | } 260 | 261 | update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await; 262 | } 263 | } 264 | }, 265 | Mode::All | Mode::Reverse | Mode::Shuffle => match query_type.clone() { 266 | QueryType::VideoLink(url) | QueryType::PlaylistLink(url) => { 267 | let urls = YouTubeRestartable::ytdl_playlist(&url, mode) 268 | .await 269 | .ok_or(ParrotError::Other("failed to fetch playlist"))?; 270 | 271 | for url in urls.into_iter() { 272 | let Ok(queue) = enqueue_track(&call, &QueryType::VideoLink(url)).await else { 273 | continue; 274 | }; 275 | update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await; 276 | } 277 | } 278 | QueryType::KeywordList(keywords_list) => { 279 | for keywords in keywords_list.into_iter() { 280 | let queue = enqueue_track(&call, &QueryType::Keywords(keywords)).await?; 281 | update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await; 282 | } 283 | } 284 | _ => { 285 | edit_response(&ctx.http, interaction, ParrotMessage::PlayAllFailed).await?; 286 | return Ok(()); 287 | } 288 | }, 289 | } 290 | 291 | let handler = call.lock().await; 292 | 293 | // refetch the queue after modification 294 | let queue = handler.queue().current_queue(); 295 | drop(handler); 296 | 297 | match queue.len().cmp(&1) { 298 | Ordering::Greater => { 299 | let estimated_time = calculate_time_until_play(&queue, mode).await.unwrap(); 300 | 301 | match (query_type, mode) { 302 | (QueryType::VideoLink(_) | QueryType::Keywords(_), Mode::Next) => { 303 | let track = queue.get(1).unwrap(); 304 | let embed = create_queued_embed(PLAY_TOP, track, estimated_time).await; 305 | 306 | edit_embed_response(&ctx.http, interaction, embed).await?; 307 | } 308 | (QueryType::VideoLink(_) | QueryType::Keywords(_), Mode::End) => { 309 | let track = queue.last().unwrap(); 310 | let embed = create_queued_embed(PLAY_QUEUE, track, estimated_time).await; 311 | 312 | edit_embed_response(&ctx.http, interaction, embed).await?; 313 | } 314 | (QueryType::PlaylistLink(_) | QueryType::KeywordList(_), _) => { 315 | edit_response(&ctx.http, interaction, ParrotMessage::PlaylistQueued).await?; 316 | } 317 | (_, _) => {} 318 | } 319 | } 320 | Ordering::Equal => { 321 | let track = queue.first().unwrap(); 322 | let embed = create_now_playing_embed(track).await; 323 | 324 | edit_embed_response(&ctx.http, interaction, embed).await?; 325 | } 326 | _ => unreachable!(), 327 | } 328 | 329 | Ok(()) 330 | } 331 | 332 | async fn calculate_time_until_play(queue: &[TrackHandle], mode: Mode) -> Option { 333 | if queue.is_empty() { 334 | return None; 335 | } 336 | 337 | let top_track = queue.first()?; 338 | let top_track_elapsed = top_track.get_info().await.unwrap().position; 339 | 340 | let top_track_duration = match top_track.metadata().duration { 341 | Some(duration) => duration, 342 | None => return Some(Duration::MAX), 343 | }; 344 | 345 | match mode { 346 | Mode::Next => Some(top_track_duration - top_track_elapsed), 347 | _ => { 348 | let center = &queue[1..queue.len() - 1]; 349 | let livestreams = 350 | center.len() - center.iter().filter_map(|t| t.metadata().duration).count(); 351 | 352 | // if any of the tracks before are livestreams, the new track will never play 353 | if livestreams > 0 { 354 | return Some(Duration::MAX); 355 | } 356 | 357 | let durations = center.iter().fold(Duration::ZERO, |acc, x| { 358 | acc + x.metadata().duration.unwrap() 359 | }); 360 | 361 | Some(durations + top_track_duration - top_track_elapsed) 362 | } 363 | } 364 | } 365 | 366 | async fn create_queued_embed( 367 | title: &str, 368 | track: &TrackHandle, 369 | estimated_time: Duration, 370 | ) -> CreateEmbed { 371 | let mut embed = CreateEmbed::default(); 372 | let metadata = track.metadata().clone(); 373 | 374 | embed.thumbnail(&metadata.thumbnail.unwrap()); 375 | 376 | embed.field( 377 | title, 378 | &format!( 379 | "[**{}**]({})", 380 | metadata.title.unwrap(), 381 | metadata.source_url.unwrap() 382 | ), 383 | false, 384 | ); 385 | 386 | let footer_text = format!( 387 | "{}{}\n{}{}", 388 | TRACK_DURATION, 389 | get_human_readable_timestamp(metadata.duration), 390 | TRACK_TIME_TO_PLAY, 391 | get_human_readable_timestamp(Some(estimated_time)) 392 | ); 393 | 394 | embed.footer(|footer| footer.text(footer_text)); 395 | embed 396 | } 397 | 398 | async fn get_track_source(query_type: QueryType) -> Result { 399 | match query_type { 400 | QueryType::VideoLink(query) => YouTubeRestartable::ytdl(query, true) 401 | .await 402 | .map_err(ParrotError::TrackFail), 403 | 404 | QueryType::Keywords(query) => YouTubeRestartable::ytdl_search(query, true) 405 | .await 406 | .map_err(ParrotError::TrackFail), 407 | 408 | _ => unreachable!(), 409 | } 410 | } 411 | 412 | async fn enqueue_track( 413 | call: &Arc>, 414 | query_type: &QueryType, 415 | ) -> Result, ParrotError> { 416 | // safeguard against ytdl dying on a private/deleted video and killing the playlist 417 | let source = get_track_source(query_type.clone()).await?; 418 | 419 | let mut handler = call.lock().await; 420 | handler.enqueue_source(source.into()); 421 | 422 | Ok(handler.queue().current_queue()) 423 | } 424 | 425 | async fn insert_track( 426 | call: &Arc>, 427 | query_type: &QueryType, 428 | idx: usize, 429 | ) -> Result, ParrotError> { 430 | let handler = call.lock().await; 431 | let queue_size = handler.queue().len(); 432 | drop(handler); 433 | 434 | if queue_size <= 1 { 435 | let queue = enqueue_track(call, query_type).await?; 436 | return Ok(queue); 437 | } 438 | 439 | verify( 440 | idx > 0 && idx <= queue_size, 441 | ParrotError::NotInRange("index", idx as isize, 1, queue_size as isize), 442 | )?; 443 | 444 | enqueue_track(call, query_type).await?; 445 | 446 | let handler = call.lock().await; 447 | handler.queue().modify_queue(|queue| { 448 | let back = queue.pop_back().unwrap(); 449 | queue.insert(idx, back); 450 | }); 451 | 452 | Ok(handler.queue().current_queue()) 453 | } 454 | 455 | async fn rotate_tracks( 456 | call: &Arc>, 457 | n: usize, 458 | ) -> Result, Box> { 459 | let handler = call.lock().await; 460 | 461 | verify( 462 | handler.queue().len() > 2, 463 | ParrotError::Other("cannot rotate queues smaller than 3 tracks"), 464 | )?; 465 | 466 | handler.queue().modify_queue(|queue| { 467 | let mut not_playing = queue.split_off(1); 468 | not_playing.rotate_right(n); 469 | queue.append(&mut not_playing); 470 | }); 471 | 472 | Ok(handler.queue().current_queue()) 473 | } 474 | -------------------------------------------------------------------------------- /src/commands/queue.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::ParrotError, 3 | guild::cache::GuildCacheMap, 4 | handlers::track_end::ModifyQueueHandler, 5 | messaging::messages::{ 6 | QUEUE_EXPIRED, QUEUE_NOTHING_IS_PLAYING, QUEUE_NOW_PLAYING, QUEUE_NO_SONGS, QUEUE_PAGE, 7 | QUEUE_PAGE_OF, QUEUE_UP_NEXT, 8 | }, 9 | utils::get_human_readable_timestamp, 10 | }; 11 | use serenity::{ 12 | builder::{CreateButton, CreateComponents, CreateEmbed}, 13 | client::Context, 14 | futures::StreamExt, 15 | model::{ 16 | application::{ 17 | component::ButtonStyle, 18 | interaction::{ 19 | application_command::ApplicationCommandInteraction, InteractionResponseType, 20 | }, 21 | }, 22 | channel::Message, 23 | id::GuildId, 24 | }, 25 | prelude::{RwLock, TypeMap}, 26 | }; 27 | use songbird::{tracks::TrackHandle, Event, TrackEvent}; 28 | use std::{ 29 | cmp::{max, min}, 30 | fmt::Write, 31 | ops::Add, 32 | sync::Arc, 33 | time::Duration, 34 | }; 35 | 36 | const EMBED_PAGE_SIZE: usize = 6; 37 | const EMBED_TIMEOUT: u64 = 3600; 38 | 39 | pub async fn queue( 40 | ctx: &Context, 41 | interaction: &mut ApplicationCommandInteraction, 42 | ) -> Result<(), ParrotError> { 43 | let guild_id = interaction.guild_id.unwrap(); 44 | let manager = songbird::get(ctx).await.unwrap(); 45 | let call = manager.get(guild_id).unwrap(); 46 | 47 | let handler = call.lock().await; 48 | let tracks = handler.queue().current_queue(); 49 | drop(handler); 50 | 51 | interaction 52 | .create_interaction_response(&ctx.http, |response| { 53 | response 54 | .kind(InteractionResponseType::ChannelMessageWithSource) 55 | .interaction_response_data(|message| { 56 | let num_pages = calculate_num_pages(&tracks); 57 | 58 | message 59 | .add_embed(create_queue_embed(&tracks, 0)) 60 | .components(|components| build_nav_btns(components, 0, num_pages)) 61 | }) 62 | }) 63 | .await?; 64 | 65 | let mut message = interaction.get_interaction_response(&ctx.http).await?; 66 | let page: Arc> = Arc::new(RwLock::new(0)); 67 | 68 | // store this interaction to context.data for later edits 69 | let mut data = ctx.data.write().await; 70 | let cache_map = data.get_mut::().unwrap(); 71 | 72 | let cache = cache_map.entry(guild_id).or_default(); 73 | cache.queue_messages.push((message.clone(), page.clone())); 74 | drop(data); 75 | 76 | // refresh the queue interaction whenever a track ends 77 | let mut handler = call.lock().await; 78 | handler.add_global_event( 79 | Event::Track(TrackEvent::End), 80 | ModifyQueueHandler { 81 | http: ctx.http.clone(), 82 | ctx_data: ctx.data.clone(), 83 | call: call.clone(), 84 | guild_id, 85 | }, 86 | ); 87 | drop(handler); 88 | 89 | let mut cib = message 90 | .await_component_interactions(ctx) 91 | .timeout(Duration::from_secs(EMBED_TIMEOUT)) 92 | .build(); 93 | 94 | while let Some(mci) = cib.next().await { 95 | let btn_id = &mci.data.custom_id; 96 | 97 | // refetch the queue in case it changed 98 | let handler = call.lock().await; 99 | let tracks = handler.queue().current_queue(); 100 | drop(handler); 101 | 102 | let num_pages = calculate_num_pages(&tracks); 103 | let mut page_wlock = page.write().await; 104 | 105 | *page_wlock = match btn_id.as_str() { 106 | "<<" => 0, 107 | "<" => min(page_wlock.saturating_sub(1), num_pages - 1), 108 | ">" => min(page_wlock.add(1), num_pages - 1), 109 | ">>" => num_pages - 1, 110 | _ => continue, 111 | }; 112 | 113 | mci.create_interaction_response(&ctx, |r| { 114 | r.kind(InteractionResponseType::UpdateMessage); 115 | r.interaction_response_data(|d| { 116 | d.add_embed(create_queue_embed(&tracks, *page_wlock)); 117 | d.components(|components| build_nav_btns(components, *page_wlock, num_pages)) 118 | }) 119 | }) 120 | .await?; 121 | } 122 | 123 | message 124 | .edit(&ctx.http, |edit| { 125 | let mut embed = CreateEmbed::default(); 126 | embed.description(QUEUE_EXPIRED); 127 | edit.set_embed(embed); 128 | edit.components(|f| f) 129 | }) 130 | .await 131 | .unwrap(); 132 | 133 | forget_queue_message(&ctx.data, &mut message, guild_id) 134 | .await 135 | .ok(); 136 | 137 | Ok(()) 138 | } 139 | 140 | pub fn create_queue_embed(tracks: &[TrackHandle], page: usize) -> CreateEmbed { 141 | let mut embed: CreateEmbed = CreateEmbed::default(); 142 | 143 | let description = if !tracks.is_empty() { 144 | let metadata = tracks[0].metadata(); 145 | embed.thumbnail(tracks[0].metadata().thumbnail.as_ref().unwrap()); 146 | 147 | format!( 148 | "[{}]({}) • `{}`", 149 | metadata.title.as_ref().unwrap(), 150 | metadata.source_url.as_ref().unwrap(), 151 | get_human_readable_timestamp(metadata.duration) 152 | ) 153 | } else { 154 | String::from(QUEUE_NOTHING_IS_PLAYING) 155 | }; 156 | 157 | embed.field(QUEUE_NOW_PLAYING, &description, false); 158 | embed.field(QUEUE_UP_NEXT, &build_queue_page(tracks, page), false); 159 | 160 | embed.footer(|f| { 161 | f.text(format!( 162 | "{} {} {} {}", 163 | QUEUE_PAGE, 164 | page + 1, 165 | QUEUE_PAGE_OF, 166 | calculate_num_pages(tracks), 167 | )) 168 | }); 169 | 170 | embed 171 | } 172 | 173 | fn build_single_nav_btn(label: &str, is_disabled: bool) -> CreateButton { 174 | CreateButton::default() 175 | .custom_id(label.to_string().to_ascii_lowercase()) 176 | .label(label) 177 | .style(ButtonStyle::Primary) 178 | .disabled(is_disabled) 179 | .to_owned() 180 | } 181 | 182 | pub fn build_nav_btns( 183 | components: &mut CreateComponents, 184 | page: usize, 185 | num_pages: usize, 186 | ) -> &mut CreateComponents { 187 | components.create_action_row(|action_row| { 188 | let (cant_left, cant_right) = (page < 1, page >= num_pages - 1); 189 | 190 | action_row 191 | .add_button(build_single_nav_btn("<<", cant_left)) 192 | .add_button(build_single_nav_btn("<", cant_left)) 193 | .add_button(build_single_nav_btn(">", cant_right)) 194 | .add_button(build_single_nav_btn(">>", cant_right)) 195 | }) 196 | } 197 | 198 | fn build_queue_page(tracks: &[TrackHandle], page: usize) -> String { 199 | let start_idx = EMBED_PAGE_SIZE * page; 200 | let queue: Vec<&TrackHandle> = tracks 201 | .iter() 202 | .skip(start_idx + 1) 203 | .take(EMBED_PAGE_SIZE) 204 | .collect(); 205 | 206 | if queue.is_empty() { 207 | return String::from(QUEUE_NO_SONGS); 208 | } 209 | 210 | let mut description = String::new(); 211 | 212 | for (i, t) in queue.iter().enumerate() { 213 | let title = t.metadata().title.as_ref().unwrap(); 214 | let url = t.metadata().source_url.as_ref().unwrap(); 215 | let duration = get_human_readable_timestamp(t.metadata().duration); 216 | 217 | let _ = writeln!( 218 | description, 219 | "`{}.` [{}]({}) • `{}`", 220 | i + start_idx + 1, 221 | title, 222 | url, 223 | duration 224 | ); 225 | } 226 | 227 | description 228 | } 229 | 230 | pub fn calculate_num_pages(tracks: &[TrackHandle]) -> usize { 231 | let num_pages = ((tracks.len() as f64 - 1.0) / EMBED_PAGE_SIZE as f64).ceil() as usize; 232 | max(1, num_pages) 233 | } 234 | 235 | pub async fn forget_queue_message( 236 | data: &Arc>, 237 | message: &mut Message, 238 | guild_id: GuildId, 239 | ) -> Result<(), ()> { 240 | let mut data_wlock = data.write().await; 241 | let cache_map = data_wlock.get_mut::().ok_or(())?; 242 | 243 | let cache = cache_map.get_mut(&guild_id).ok_or(())?; 244 | cache.queue_messages.retain(|(m, _)| m.id != message.id); 245 | 246 | Ok(()) 247 | } 248 | -------------------------------------------------------------------------------- /src/commands/remove.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::{verify, ParrotError}, 3 | handlers::track_end::update_queue_messages, 4 | messaging::message::ParrotMessage, 5 | messaging::messages::REMOVED_QUEUE, 6 | utils::create_embed_response, 7 | utils::create_response, 8 | }; 9 | use serenity::{ 10 | builder::CreateEmbed, client::Context, 11 | model::application::interaction::application_command::ApplicationCommandInteraction, 12 | }; 13 | use songbird::tracks::TrackHandle; 14 | use std::cmp::min; 15 | 16 | pub async fn remove( 17 | ctx: &Context, 18 | interaction: &mut ApplicationCommandInteraction, 19 | ) -> Result<(), ParrotError> { 20 | let guild_id = interaction.guild_id.unwrap(); 21 | let manager = songbird::get(ctx).await.unwrap(); 22 | let call = manager.get(guild_id).unwrap(); 23 | 24 | let args = interaction.data.options.clone(); 25 | 26 | let remove_index = args 27 | .first() 28 | .unwrap() 29 | .value 30 | .as_ref() 31 | .unwrap() 32 | .as_u64() 33 | .unwrap() as usize; 34 | 35 | let remove_until = match args.get(1) { 36 | Some(arg) => arg.value.as_ref().unwrap().as_u64().unwrap() as usize, 37 | None => remove_index, 38 | }; 39 | 40 | let handler = call.lock().await; 41 | let queue = handler.queue().current_queue(); 42 | 43 | let queue_len = queue.len(); 44 | let remove_until = min(remove_until, queue_len.saturating_sub(1)); 45 | 46 | verify(queue_len > 1, ParrotError::QueueEmpty)?; 47 | verify( 48 | remove_index < queue_len, 49 | ParrotError::NotInRange("index", remove_index as isize, 1, queue_len as isize), 50 | )?; 51 | verify( 52 | remove_until >= remove_index, 53 | ParrotError::NotInRange( 54 | "until", 55 | remove_until as isize, 56 | remove_index as isize, 57 | queue_len as isize, 58 | ), 59 | )?; 60 | 61 | let track = queue.get(remove_index).unwrap(); 62 | 63 | handler.queue().modify_queue(|v| { 64 | v.drain(remove_index..=remove_until); 65 | }); 66 | 67 | // refetch the queue after modification 68 | let queue = handler.queue().current_queue(); 69 | drop(handler); 70 | 71 | if remove_until == remove_index { 72 | let embed = create_remove_enqueued_embed(track).await; 73 | create_embed_response(&ctx.http, interaction, embed).await?; 74 | } else { 75 | create_response(&ctx.http, interaction, ParrotMessage::RemoveMultiple).await?; 76 | } 77 | 78 | update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await; 79 | Ok(()) 80 | } 81 | 82 | async fn create_remove_enqueued_embed(track: &TrackHandle) -> CreateEmbed { 83 | let mut embed = CreateEmbed::default(); 84 | let metadata = track.metadata().clone(); 85 | 86 | embed.field( 87 | REMOVED_QUEUE, 88 | &format!( 89 | "[**{}**]({})", 90 | metadata.title.unwrap(), 91 | metadata.source_url.unwrap() 92 | ), 93 | false, 94 | ); 95 | embed.thumbnail(&metadata.thumbnail.unwrap()); 96 | 97 | embed 98 | } 99 | -------------------------------------------------------------------------------- /src/commands/repeat.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::ParrotError, messaging::message::ParrotMessage, messaging::messages::FAIL_LOOP, 3 | utils::create_response, 4 | }; 5 | use serenity::{ 6 | client::Context, 7 | model::application::interaction::application_command::ApplicationCommandInteraction, 8 | }; 9 | use songbird::tracks::{LoopState, TrackHandle}; 10 | 11 | pub async fn repeat( 12 | ctx: &Context, 13 | interaction: &mut ApplicationCommandInteraction, 14 | ) -> Result<(), ParrotError> { 15 | let guild_id = interaction.guild_id.unwrap(); 16 | let manager = songbird::get(ctx).await.unwrap(); 17 | let call = manager.get(guild_id).unwrap(); 18 | 19 | let handler = call.lock().await; 20 | let track = handler.queue().current().unwrap(); 21 | 22 | let was_looping = track.get_info().await.unwrap().loops == LoopState::Infinite; 23 | let toggler = if was_looping { 24 | TrackHandle::disable_loop 25 | } else { 26 | TrackHandle::enable_loop 27 | }; 28 | 29 | match toggler(&track) { 30 | Ok(_) if was_looping => { 31 | create_response(&ctx.http, interaction, ParrotMessage::LoopDisable).await 32 | } 33 | Ok(_) if !was_looping => { 34 | create_response(&ctx.http, interaction, ParrotMessage::LoopEnable).await 35 | } 36 | _ => Err(ParrotError::Other(FAIL_LOOP)), 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/resume.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::{verify, ParrotError}, 3 | messaging::message::ParrotMessage, 4 | utils::create_response, 5 | }; 6 | use serenity::{ 7 | client::Context, 8 | model::application::interaction::application_command::ApplicationCommandInteraction, 9 | }; 10 | 11 | pub async fn resume( 12 | ctx: &Context, 13 | interaction: &mut ApplicationCommandInteraction, 14 | ) -> Result<(), ParrotError> { 15 | let guild_id = interaction.guild_id.unwrap(); 16 | let manager = songbird::get(ctx).await.unwrap(); 17 | let call = manager.get(guild_id).unwrap(); 18 | 19 | let handler = call.lock().await; 20 | let queue = handler.queue(); 21 | 22 | verify(!queue.is_empty(), ParrotError::NothingPlaying)?; 23 | verify(queue.resume(), ParrotError::Other("Failed resuming track"))?; 24 | 25 | create_response(&ctx.http, interaction, ParrotMessage::Resume).await 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/seek.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::{verify, ParrotError}, 3 | messaging::message::ParrotMessage, 4 | messaging::messages::{FAIL_MINUTES_PARSING, FAIL_SECONDS_PARSING}, 5 | utils::create_response, 6 | }; 7 | use serenity::{ 8 | client::Context, 9 | model::application::interaction::application_command::ApplicationCommandInteraction, 10 | }; 11 | use std::time::Duration; 12 | 13 | pub async fn seek( 14 | ctx: &Context, 15 | interaction: &mut ApplicationCommandInteraction, 16 | ) -> Result<(), ParrotError> { 17 | let guild_id = interaction.guild_id.unwrap(); 18 | let manager = songbird::get(ctx).await.unwrap(); 19 | let call = manager.get(guild_id).unwrap(); 20 | 21 | let args = interaction.data.options.clone(); 22 | let seek_time = args.first().unwrap().value.as_ref().unwrap(); 23 | 24 | let timestamp_str = seek_time.as_str().unwrap(); 25 | let mut units_iter = timestamp_str.split(':'); 26 | 27 | let minutes = units_iter.next().and_then(|c| c.parse::().ok()); 28 | let minutes = verify(minutes, ParrotError::Other(FAIL_MINUTES_PARSING))?; 29 | 30 | let seconds = units_iter.next().and_then(|c| c.parse::().ok()); 31 | let seconds = verify(seconds, ParrotError::Other(FAIL_SECONDS_PARSING))?; 32 | 33 | let timestamp = minutes * 60 + seconds; 34 | 35 | let handler = call.lock().await; 36 | let track = handler 37 | .queue() 38 | .current() 39 | .ok_or(ParrotError::NothingPlaying)?; 40 | drop(handler); 41 | 42 | track.seek_time(Duration::from_secs(timestamp)).unwrap(); 43 | 44 | create_response( 45 | &ctx.http, 46 | interaction, 47 | ParrotMessage::Seek { 48 | timestamp: timestamp_str.to_owned(), 49 | }, 50 | ) 51 | .await 52 | } 53 | -------------------------------------------------------------------------------- /src/commands/shuffle.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::ParrotError, handlers::track_end::update_queue_messages, 3 | messaging::message::ParrotMessage, utils::create_response, 4 | }; 5 | use rand::Rng; 6 | use serenity::{ 7 | client::Context, 8 | model::application::interaction::application_command::ApplicationCommandInteraction, 9 | }; 10 | 11 | pub async fn shuffle( 12 | ctx: &Context, 13 | interaction: &mut ApplicationCommandInteraction, 14 | ) -> Result<(), ParrotError> { 15 | let guild_id = interaction.guild_id.unwrap(); 16 | let manager = songbird::get(ctx).await.unwrap(); 17 | let call = manager.get(guild_id).unwrap(); 18 | 19 | let handler = call.lock().await; 20 | handler.queue().modify_queue(|queue| { 21 | // skip the first track on queue because it's being played 22 | fisher_yates( 23 | queue.make_contiguous()[1..].as_mut(), 24 | &mut rand::thread_rng(), 25 | ) 26 | }); 27 | 28 | // refetch the queue after modification 29 | let queue = handler.queue().current_queue(); 30 | drop(handler); 31 | 32 | create_response(&ctx.http, interaction, ParrotMessage::Shuffle).await?; 33 | update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await; 34 | Ok(()) 35 | } 36 | 37 | fn fisher_yates(values: &mut [T], mut rng: R) 38 | where 39 | R: rand::RngCore + Sized, 40 | { 41 | let mut index = values.len(); 42 | while index >= 2 { 43 | index -= 1; 44 | values.swap(index, rng.gen_range(0..(index + 1))); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/skip.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::{verify, ParrotError}, 3 | messaging::message::ParrotMessage, 4 | utils::create_response, 5 | }; 6 | use serenity::{ 7 | client::Context, 8 | model::application::interaction::application_command::ApplicationCommandInteraction, 9 | }; 10 | use songbird::{tracks::TrackHandle, Call}; 11 | use std::cmp::min; 12 | use tokio::sync::MutexGuard; 13 | 14 | pub async fn skip( 15 | ctx: &Context, 16 | interaction: &mut ApplicationCommandInteraction, 17 | ) -> Result<(), ParrotError> { 18 | let guild_id = interaction.guild_id.unwrap(); 19 | let manager = songbird::get(ctx).await.unwrap(); 20 | let call = manager.get(guild_id).unwrap(); 21 | 22 | let args = interaction.data.options.clone(); 23 | let to_skip = match args.first() { 24 | Some(arg) => arg.value.as_ref().unwrap().as_u64().unwrap() as usize, 25 | None => 1, 26 | }; 27 | 28 | let handler = call.lock().await; 29 | let queue = handler.queue(); 30 | 31 | verify(!queue.is_empty(), ParrotError::NothingPlaying)?; 32 | 33 | let tracks_to_skip = min(to_skip, queue.len()); 34 | 35 | handler.queue().modify_queue(|v| { 36 | v.drain(1..tracks_to_skip); 37 | }); 38 | 39 | force_skip_top_track(&handler).await?; 40 | create_skip_response(ctx, interaction, &handler, tracks_to_skip).await 41 | } 42 | 43 | pub async fn create_skip_response( 44 | ctx: &Context, 45 | interaction: &mut ApplicationCommandInteraction, 46 | handler: &MutexGuard<'_, Call>, 47 | tracks_to_skip: usize, 48 | ) -> Result<(), ParrotError> { 49 | match handler.queue().current() { 50 | Some(track) => { 51 | create_response( 52 | &ctx.http, 53 | interaction, 54 | ParrotMessage::SkipTo { 55 | title: track.metadata().title.as_ref().unwrap().to_owned(), 56 | url: track.metadata().source_url.as_ref().unwrap().to_owned(), 57 | }, 58 | ) 59 | .await 60 | } 61 | None => { 62 | if tracks_to_skip > 1 { 63 | create_response(&ctx.http, interaction, ParrotMessage::SkipAll).await 64 | } else { 65 | create_response(&ctx.http, interaction, ParrotMessage::Skip).await 66 | } 67 | } 68 | } 69 | } 70 | 71 | pub async fn force_skip_top_track( 72 | handler: &MutexGuard<'_, Call>, 73 | ) -> Result, ParrotError> { 74 | // this is an odd sequence of commands to ensure the queue is properly updated 75 | // apparently, skipping/stopping a track takes a while to remove it from the queue 76 | // also, manually removing tracks doesn't trigger the next track to play 77 | // so first, stop the top song, manually remove it and then resume playback 78 | handler.queue().current().unwrap().stop().ok(); 79 | handler.queue().dequeue(0); 80 | handler.queue().resume().ok(); 81 | 82 | Ok(handler.queue().current_queue()) 83 | } 84 | -------------------------------------------------------------------------------- /src/commands/stop.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::{verify, ParrotError}, 3 | handlers::track_end::update_queue_messages, 4 | messaging::message::ParrotMessage, 5 | utils::create_response, 6 | }; 7 | use serenity::{ 8 | client::Context, 9 | model::application::interaction::application_command::ApplicationCommandInteraction, 10 | }; 11 | 12 | pub async fn stop( 13 | ctx: &Context, 14 | interaction: &mut ApplicationCommandInteraction, 15 | ) -> Result<(), ParrotError> { 16 | let guild_id = interaction.guild_id.unwrap(); 17 | let manager = songbird::get(ctx).await.unwrap(); 18 | let call = manager.get(guild_id).unwrap(); 19 | 20 | let handler = call.lock().await; 21 | let queue = handler.queue(); 22 | 23 | verify(!queue.is_empty(), ParrotError::NothingPlaying)?; 24 | queue.stop(); 25 | 26 | // refetch the queue after modification 27 | let queue = handler.queue().current_queue(); 28 | drop(handler); 29 | 30 | create_response(&ctx.http, interaction, ParrotMessage::Stop).await?; 31 | update_queue_messages(&ctx.http, &ctx.data, &queue, guild_id).await; 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/summon.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | connection::get_voice_channel_for_user, 3 | errors::ParrotError, 4 | handlers::{IdleHandler, TrackEndHandler}, 5 | messaging::message::ParrotMessage, 6 | utils::create_response, 7 | }; 8 | use serenity::{ 9 | client::Context, 10 | model::{ 11 | application::interaction::application_command::ApplicationCommandInteraction, id::ChannelId, 12 | }, 13 | prelude::Mentionable, 14 | }; 15 | use songbird::{Event, TrackEvent}; 16 | use std::time::Duration; 17 | 18 | pub async fn summon( 19 | ctx: &Context, 20 | interaction: &mut ApplicationCommandInteraction, 21 | send_reply: bool, 22 | ) -> Result<(), ParrotError> { 23 | let guild_id = interaction.guild_id.unwrap(); 24 | let guild = ctx.cache.guild(guild_id).unwrap(); 25 | 26 | let manager = songbird::get(ctx).await.unwrap(); 27 | let channel_opt = get_voice_channel_for_user(&guild, &interaction.user.id); 28 | let channel_id = channel_opt.unwrap(); 29 | 30 | if let Some(call) = manager.get(guild.id) { 31 | let handler = call.lock().await; 32 | let has_current_connection = handler.current_connection().is_some(); 33 | 34 | if has_current_connection && send_reply { 35 | // bot is in another channel 36 | let bot_channel_id: ChannelId = handler.current_channel().unwrap().0.into(); 37 | return Err(ParrotError::AlreadyConnected(bot_channel_id.mention())); 38 | } 39 | } 40 | 41 | // join the channel 42 | manager.join(guild.id, channel_id).await.1.unwrap(); 43 | 44 | // unregister existing events and register idle notifier 45 | if let Some(call) = manager.get(guild.id) { 46 | let mut handler = call.lock().await; 47 | 48 | handler.remove_all_global_events(); 49 | 50 | handler.add_global_event( 51 | Event::Periodic(Duration::from_secs(1), None), 52 | IdleHandler { 53 | http: ctx.http.clone(), 54 | manager, 55 | interaction: interaction.clone(), 56 | limit: 60 * 10, 57 | count: Default::default(), 58 | }, 59 | ); 60 | 61 | handler.add_global_event( 62 | Event::Track(TrackEvent::End), 63 | TrackEndHandler { 64 | guild_id: guild.id, 65 | call: call.clone(), 66 | ctx_data: ctx.data.clone(), 67 | }, 68 | ); 69 | } 70 | 71 | if send_reply { 72 | return create_response( 73 | &ctx.http, 74 | interaction, 75 | ParrotMessage::Summon { 76 | mention: channel_id.mention(), 77 | }, 78 | ) 79 | .await; 80 | } 81 | 82 | Ok(()) 83 | } 84 | -------------------------------------------------------------------------------- /src/commands/version.rs: -------------------------------------------------------------------------------- 1 | use crate::{errors::ParrotError, messaging::message::ParrotMessage, utils::create_response}; 2 | use serenity::{ 3 | client::Context, 4 | model::application::interaction::application_command::ApplicationCommandInteraction, 5 | }; 6 | 7 | pub async fn version( 8 | ctx: &Context, 9 | interaction: &mut ApplicationCommandInteraction, 10 | ) -> Result<(), ParrotError> { 11 | let current = option_env!("CARGO_PKG_VERSION").unwrap_or_else(|| "Unknown"); 12 | create_response( 13 | &ctx.http, 14 | interaction, 15 | ParrotMessage::Version { 16 | current: current.to_owned(), 17 | }, 18 | ) 19 | .await 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/voteskip.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | commands::skip::{create_skip_response, force_skip_top_track}, 3 | connection::get_voice_channel_for_user, 4 | errors::{verify, ParrotError}, 5 | guild::cache::GuildCacheMap, 6 | messaging::message::ParrotMessage, 7 | utils::create_response, 8 | }; 9 | use serenity::{ 10 | client::Context, 11 | model::{ 12 | application::interaction::application_command::ApplicationCommandInteraction, id::GuildId, 13 | }, 14 | prelude::{Mentionable, RwLock, TypeMap}, 15 | }; 16 | use std::{collections::HashSet, sync::Arc}; 17 | 18 | pub async fn voteskip( 19 | ctx: &Context, 20 | interaction: &mut ApplicationCommandInteraction, 21 | ) -> Result<(), ParrotError> { 22 | let guild_id = interaction.guild_id.unwrap(); 23 | let bot_channel_id = get_voice_channel_for_user( 24 | &ctx.cache.guild(guild_id).unwrap(), 25 | &ctx.cache.current_user_id(), 26 | ) 27 | .unwrap(); 28 | let manager = songbird::get(ctx).await.unwrap(); 29 | let call = manager.get(guild_id).unwrap(); 30 | 31 | let handler = call.lock().await; 32 | let queue = handler.queue(); 33 | 34 | verify(!queue.is_empty(), ParrotError::NothingPlaying)?; 35 | 36 | let mut data = ctx.data.write().await; 37 | let cache_map = data.get_mut::().unwrap(); 38 | 39 | let cache = cache_map.entry(guild_id).or_default(); 40 | cache.current_skip_votes.insert(interaction.user.id); 41 | 42 | let guild_users = ctx.cache.guild(guild_id).unwrap().voice_states; 43 | let channel_guild_users = guild_users 44 | .into_values() 45 | .filter(|v| v.channel_id.unwrap() == bot_channel_id); 46 | let skip_threshold = channel_guild_users.count() / 2; 47 | 48 | if cache.current_skip_votes.len() >= skip_threshold { 49 | force_skip_top_track(&handler).await?; 50 | create_skip_response(ctx, interaction, &handler, 1).await 51 | } else { 52 | create_response( 53 | &ctx.http, 54 | interaction, 55 | ParrotMessage::VoteSkip { 56 | mention: interaction.user.id.mention(), 57 | missing: skip_threshold - cache.current_skip_votes.len(), 58 | }, 59 | ) 60 | .await 61 | } 62 | } 63 | 64 | pub async fn forget_skip_votes(data: &Arc>, guild_id: GuildId) -> Result<(), ()> { 65 | let mut data = data.write().await; 66 | 67 | let cache_map = data.get_mut::().ok_or(())?; 68 | let cache = cache_map.get_mut(&guild_id).ok_or(())?; 69 | cache.current_skip_votes = HashSet::new(); 70 | 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /src/connection.rs: -------------------------------------------------------------------------------- 1 | use serenity::model::{ 2 | guild::Guild, 3 | id::{ChannelId, UserId}, 4 | }; 5 | 6 | pub enum Connection { 7 | User(ChannelId), 8 | Bot(ChannelId), 9 | Mutual(ChannelId, ChannelId), 10 | Separate(ChannelId, ChannelId), 11 | Neither, 12 | } 13 | 14 | pub fn check_voice_connections(guild: &Guild, user_id: &UserId, bot_id: &UserId) -> Connection { 15 | let user_channel = get_voice_channel_for_user(guild, user_id); 16 | let bot_channel = get_voice_channel_for_user(guild, bot_id); 17 | 18 | if let (Some(bot_id), Some(user_id)) = (bot_channel, user_channel) { 19 | if bot_id.0 == user_id.0 { 20 | Connection::Mutual(bot_id, user_id) 21 | } else { 22 | Connection::Separate(bot_id, user_id) 23 | } 24 | } else if let (Some(bot_id), None) = (bot_channel, user_channel) { 25 | Connection::Bot(bot_id) 26 | } else if let (None, Some(user_id)) = (bot_channel, user_channel) { 27 | Connection::User(user_id) 28 | } else { 29 | Connection::Neither 30 | } 31 | } 32 | 33 | pub fn get_voice_channel_for_user(guild: &Guild, user_id: &UserId) -> Option { 34 | guild 35 | .voice_states 36 | .get(user_id) 37 | .and_then(|voice_state| voice_state.channel_id) 38 | } 39 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::messaging::messages::{ 2 | FAIL_ANOTHER_CHANNEL, FAIL_AUTHOR_DISCONNECTED, FAIL_AUTHOR_NOT_FOUND, 3 | FAIL_NO_VOICE_CONNECTION, FAIL_WRONG_CHANNEL, NOTHING_IS_PLAYING, QUEUE_IS_EMPTY, 4 | TRACK_INAPPROPRIATE, TRACK_NOT_FOUND, 5 | }; 6 | use rspotify::ClientError as RSpotifyClientError; 7 | use serenity::{model::mention::Mention, prelude::SerenityError}; 8 | use songbird::input::error::Error as InputError; 9 | use std::fmt::{Debug, Display}; 10 | use std::{error::Error, fmt}; 11 | 12 | /// A common error enum returned by most of the crate's functions within a [`Result`]. 13 | #[derive(Debug)] 14 | pub enum ParrotError { 15 | Other(&'static str), 16 | QueueEmpty, 17 | NotInRange(&'static str, isize, isize, isize), 18 | NotConnected, 19 | AuthorDisconnected(Mention), 20 | WrongVoiceChannel, 21 | AuthorNotFound, 22 | NothingPlaying, 23 | TrackFail(InputError), 24 | AlreadyConnected(Mention), 25 | Serenity(SerenityError), 26 | RSpotify(RSpotifyClientError), 27 | IO(std::io::Error), 28 | Serde(serde_json::Error), 29 | } 30 | 31 | /// `ParrotError` implements the [`Debug`] and [`Display`] traits 32 | /// meaning it implements the [`std::error::Error`] trait. 33 | /// This just makes it explicit. 34 | impl Error for ParrotError {} 35 | 36 | /// Implementation of the [`Display`] trait for the [`ParrotError`] enum. 37 | /// Errors are formatted with this and then sent as responses to the interaction. 38 | impl Display for ParrotError { 39 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 40 | match self { 41 | Self::Other(msg) => f.write_str(msg), 42 | Self::QueueEmpty => f.write_str(QUEUE_IS_EMPTY), 43 | Self::NotInRange(param, value, lower, upper) => f.write_str(&format!( 44 | "`{param}` should be between {lower} and {upper} but was {value}" 45 | )), 46 | Self::NotConnected => f.write_str(FAIL_NO_VOICE_CONNECTION), 47 | Self::AuthorDisconnected(mention) => { 48 | f.write_fmt(format_args!("{} {}", FAIL_AUTHOR_DISCONNECTED, mention)) 49 | } 50 | Self::WrongVoiceChannel => f.write_str(FAIL_WRONG_CHANNEL), 51 | Self::AuthorNotFound => f.write_str(FAIL_AUTHOR_NOT_FOUND), 52 | Self::AlreadyConnected(mention) => { 53 | f.write_fmt(format_args!("{} {}", FAIL_ANOTHER_CHANNEL, mention)) 54 | } 55 | Self::NothingPlaying => f.write_str(NOTHING_IS_PLAYING), 56 | Self::TrackFail(err) => match err { 57 | InputError::Json { 58 | error: _, 59 | parsed_text, 60 | } => { 61 | if parsed_text.contains("Sign in to confirm your age") { 62 | f.write_str(TRACK_INAPPROPRIATE) 63 | } else { 64 | f.write_str(TRACK_NOT_FOUND) 65 | } 66 | } 67 | _ => f.write_str(&format!("{err}")), 68 | }, 69 | Self::Serenity(err) => f.write_str(&format!("{err}")), 70 | Self::RSpotify(err) => f.write_str(&format!("{err}")), 71 | Self::IO(err) => f.write_str(&format!("{err}")), 72 | Self::Serde(err) => f.write_str(&format!("{err}")), 73 | } 74 | } 75 | } 76 | 77 | /// Implementation of the [`PartialEq`] trait for the [`ParrotError`] enum. 78 | /// For some enum variants, values are considered equal when their inner values 79 | /// are equal and for others when they are of the same type. 80 | impl PartialEq for ParrotError { 81 | fn eq(&self, other: &Self) -> bool { 82 | match (self, other) { 83 | (Self::Other(l0), Self::Other(r0)) => l0 == r0, 84 | (Self::NotInRange(l0, l1, l2, l3), Self::NotInRange(r0, r1, r2, r3)) => { 85 | l0 == r0 && l1 == r1 && l2 == r2 && l3 == r3 86 | } 87 | (Self::AuthorDisconnected(l0), Self::AuthorDisconnected(r0)) => { 88 | l0.to_string() == r0.to_string() 89 | } 90 | (Self::AlreadyConnected(l0), Self::AlreadyConnected(r0)) => { 91 | l0.to_string() == r0.to_string() 92 | } 93 | (Self::Serenity(l0), Self::Serenity(r0)) => format!("{l0:?}") == format!("{r0:?}"), 94 | _ => core::mem::discriminant(self) == core::mem::discriminant(other), 95 | } 96 | } 97 | } 98 | 99 | /// Provides an implementation to convert a [`std::io::Error`] to a [`ParrotError`]. 100 | impl From for ParrotError { 101 | fn from(err: std::io::Error) -> Self { 102 | Self::IO(err) 103 | } 104 | } 105 | 106 | /// Provides an implementation to convert a [`serde_json::Error`] to a [`ParrotError`]. 107 | impl From for ParrotError { 108 | fn from(err: serde_json::Error) -> Self { 109 | Self::Serde(err) 110 | } 111 | } 112 | 113 | /// Provides an implementation to convert a [`SerenityError`] to a [`ParrotError`]. 114 | impl From for ParrotError { 115 | fn from(err: SerenityError) -> Self { 116 | match err { 117 | SerenityError::NotInRange(param, value, lower, upper) => { 118 | Self::NotInRange(param, value as isize, lower as isize, upper as isize) 119 | } 120 | SerenityError::Other(msg) => Self::Other(msg), 121 | _ => Self::Serenity(err), 122 | } 123 | } 124 | } 125 | 126 | /// Provides an implementation to convert a rspotify [`ClientError`] to a [`ParrotError`]. 127 | impl From for ParrotError { 128 | fn from(err: RSpotifyClientError) -> ParrotError { 129 | ParrotError::RSpotify(err) 130 | } 131 | } 132 | 133 | /// Types that implement this trait can be tested as true or false and also provide 134 | /// a way of unpacking themselves. 135 | pub trait Verifiable { 136 | fn to_bool(&self) -> bool; 137 | fn unpack(self) -> T; 138 | } 139 | 140 | impl Verifiable for bool { 141 | fn to_bool(&self) -> bool { 142 | *self 143 | } 144 | 145 | fn unpack(self) -> bool { 146 | self 147 | } 148 | } 149 | 150 | impl Verifiable for Option { 151 | fn to_bool(&self) -> bool { 152 | self.is_some() 153 | } 154 | 155 | fn unpack(self) -> T { 156 | self.unwrap() 157 | } 158 | } 159 | 160 | impl Verifiable for Result 161 | where 162 | E: Debug, 163 | { 164 | fn to_bool(&self) -> bool { 165 | self.is_ok() 166 | } 167 | 168 | fn unpack(self) -> T { 169 | self.unwrap() 170 | } 171 | } 172 | 173 | /// Verifies if a value is true (or equivalent). 174 | /// Returns an [`Err`] with the given error or the value wrapped in [`Ok`]. 175 | pub fn verify>(verifiable: T, err: ParrotError) -> Result { 176 | if verifiable.to_bool() { 177 | Ok(verifiable.unpack()) 178 | } else { 179 | Err(err) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/guild/cache.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{HashMap, HashSet}, 3 | sync::Arc, 4 | }; 5 | 6 | use serenity::{ 7 | model::{ 8 | channel::Message, 9 | id::{GuildId, UserId}, 10 | }, 11 | prelude::{RwLock, TypeMapKey}, 12 | }; 13 | 14 | type QueueMessage = (Message, Arc>); 15 | 16 | #[derive(Default)] 17 | pub struct GuildCache { 18 | pub queue_messages: Vec, 19 | pub current_skip_votes: HashSet, 20 | } 21 | 22 | pub struct GuildCacheMap; 23 | 24 | impl TypeMapKey for GuildCacheMap { 25 | type Value = HashMap; 26 | } 27 | -------------------------------------------------------------------------------- /src/guild/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cache; 2 | pub mod settings; 3 | -------------------------------------------------------------------------------- /src/guild/settings.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use serde::{Deserialize, Serialize}; 3 | use serenity::{model::id::GuildId, prelude::TypeMapKey}; 4 | use std::{ 5 | collections::{HashMap, HashSet}, 6 | env, 7 | fs::{create_dir_all, OpenOptions}, 8 | io::{BufReader, BufWriter}, 9 | path::Path, 10 | }; 11 | 12 | use crate::errors::ParrotError; 13 | 14 | const DEFAULT_SETTINGS_PATH: &str = "data/settings"; 15 | const DEFAULT_ALLOWED_DOMAINS: [&str; 2] = ["youtube.com", "youtu.be"]; 16 | 17 | lazy_static! { 18 | static ref SETTINGS_PATH: String = 19 | env::var("SETTINGS_PATH").unwrap_or(DEFAULT_SETTINGS_PATH.to_string()); 20 | } 21 | 22 | #[derive(Deserialize, Serialize)] 23 | pub struct GuildSettings { 24 | pub guild_id: GuildId, 25 | pub autopause: bool, 26 | pub allowed_domains: HashSet, 27 | pub banned_domains: HashSet, 28 | } 29 | 30 | impl GuildSettings { 31 | pub fn new(guild_id: GuildId) -> GuildSettings { 32 | let allowed_domains: HashSet = DEFAULT_ALLOWED_DOMAINS 33 | .iter() 34 | .map(|d| d.to_string()) 35 | .collect(); 36 | 37 | GuildSettings { 38 | guild_id, 39 | autopause: false, 40 | allowed_domains, 41 | banned_domains: HashSet::new(), 42 | } 43 | } 44 | 45 | pub fn load_if_exists(&mut self) -> Result<(), ParrotError> { 46 | let path = format!("{}/{}.json", SETTINGS_PATH.as_str(), self.guild_id); 47 | if !Path::new(&path).exists() { 48 | return Ok(()); 49 | } 50 | self.load() 51 | } 52 | 53 | pub fn load(&mut self) -> Result<(), ParrotError> { 54 | let path = format!("{}/{}.json", SETTINGS_PATH.as_str(), self.guild_id); 55 | let file = OpenOptions::new().read(true).open(path)?; 56 | let reader = BufReader::new(file); 57 | *self = serde_json::from_reader::<_, GuildSettings>(reader)?; 58 | Ok(()) 59 | } 60 | 61 | pub fn save(&self) -> Result<(), ParrotError> { 62 | create_dir_all(SETTINGS_PATH.as_str())?; 63 | let path = format!("{}/{}.json", SETTINGS_PATH.as_str(), self.guild_id); 64 | 65 | let file = OpenOptions::new() 66 | .write(true) 67 | .truncate(true) 68 | .create(true) 69 | .open(path)?; 70 | 71 | let writer = BufWriter::new(file); 72 | serde_json::to_writer(writer, self)?; 73 | Ok(()) 74 | } 75 | 76 | pub fn toggle_autopause(&mut self) { 77 | self.autopause = !self.autopause; 78 | } 79 | 80 | pub fn set_allowed_domains(&mut self, allowed_str: &str) { 81 | let allowed = allowed_str 82 | .split(';') 83 | .filter(|s| !s.is_empty()) 84 | .map(|s| s.to_string()) 85 | .collect(); 86 | 87 | self.allowed_domains = allowed; 88 | } 89 | 90 | pub fn set_banned_domains(&mut self, banned_str: &str) { 91 | let banned = banned_str 92 | .split(';') 93 | .filter(|s| !s.is_empty()) 94 | .map(|s| s.to_string()) 95 | .collect(); 96 | 97 | self.banned_domains = banned; 98 | } 99 | 100 | pub fn update_domains(&mut self) { 101 | if !self.allowed_domains.is_empty() && !self.banned_domains.is_empty() { 102 | self.banned_domains.clear(); 103 | } 104 | 105 | if self.allowed_domains.is_empty() && self.banned_domains.is_empty() { 106 | self.allowed_domains = DEFAULT_ALLOWED_DOMAINS 107 | .iter() 108 | .map(|d| d.to_string()) 109 | .collect(); 110 | 111 | self.banned_domains.clear(); 112 | } 113 | } 114 | } 115 | 116 | pub struct GuildSettingsMap; 117 | 118 | impl TypeMapKey for GuildSettingsMap { 119 | type Value = HashMap; 120 | } 121 | -------------------------------------------------------------------------------- /src/handlers/idle.rs: -------------------------------------------------------------------------------- 1 | use serenity::{ 2 | async_trait, http::Http, 3 | model::application::interaction::application_command::ApplicationCommandInteraction, 4 | }; 5 | use songbird::{tracks::PlayMode, Event, EventContext, EventHandler, Songbird}; 6 | use std::sync::{ 7 | atomic::{AtomicUsize, Ordering}, 8 | Arc, 9 | }; 10 | 11 | use crate::messaging::messages::IDLE_ALERT; 12 | 13 | pub struct IdleHandler { 14 | pub http: Arc, 15 | pub manager: Arc, 16 | pub interaction: ApplicationCommandInteraction, 17 | pub limit: usize, 18 | pub count: Arc, 19 | } 20 | 21 | #[async_trait] 22 | impl EventHandler for IdleHandler { 23 | async fn act(&self, ctx: &EventContext<'_>) -> Option { 24 | let EventContext::Track(track_list) = ctx else { 25 | return None; 26 | }; 27 | 28 | // looks like the track list isn't ordered here, so the first track in the list isn't 29 | // guaranteed to be the first track in the actual queue, so search the entire list 30 | let bot_is_playing = track_list 31 | .iter() 32 | .any(|track| matches!(track.0.playing, PlayMode::Play)); 33 | 34 | // if there's a track playing, then reset the counter 35 | if bot_is_playing { 36 | self.count.store(0, Ordering::Relaxed); 37 | return None; 38 | } 39 | 40 | if self.count.fetch_add(1, Ordering::Relaxed) >= self.limit { 41 | let guild_id = self.interaction.guild_id?; 42 | 43 | if self.manager.remove(guild_id).await.is_ok() { 44 | self.interaction 45 | .channel_id 46 | .say(&self.http, IDLE_ALERT) 47 | .await 48 | .unwrap(); 49 | } 50 | } 51 | 52 | None 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod idle; 2 | pub mod serenity; 3 | pub mod track_end; 4 | 5 | pub use self::idle::IdleHandler; 6 | pub use self::serenity::SerenityHandler; 7 | pub use self::track_end::TrackEndHandler; 8 | -------------------------------------------------------------------------------- /src/handlers/serenity.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | commands::{ 3 | autopause::*, clear::*, leave::*, manage_sources::*, now_playing::*, pause::*, play::*, 4 | queue::*, remove::*, repeat::*, resume::*, seek::*, shuffle::*, skip::*, stop::*, 5 | summon::*, version::*, voteskip::*, 6 | }, 7 | connection::{check_voice_connections, Connection}, 8 | errors::ParrotError, 9 | guild::settings::{GuildSettings, GuildSettingsMap}, 10 | handlers::track_end::update_queue_messages, 11 | sources::spotify::{Spotify, SPOTIFY}, 12 | utils::create_response_text, 13 | }; 14 | use serenity::{ 15 | async_trait, 16 | client::{Context, EventHandler}, 17 | model::{ 18 | application::command::{Command, CommandOptionType}, 19 | application::interaction::{ 20 | application_command::ApplicationCommandInteraction, Interaction, 21 | }, 22 | gateway::Ready, 23 | id::GuildId, 24 | prelude::{Activity, VoiceState}, 25 | }, 26 | prelude::Mentionable, 27 | }; 28 | 29 | pub struct SerenityHandler; 30 | 31 | #[async_trait] 32 | impl EventHandler for SerenityHandler { 33 | async fn ready(&self, ctx: Context, ready: Ready) { 34 | println!("🦜 {} is connected!", ready.user.name); 35 | 36 | // sets parrot activity status message to /play 37 | let activity = Activity::listening("/play"); 38 | ctx.set_activity(activity).await; 39 | 40 | // attempts to authenticate to spotify 41 | *SPOTIFY.lock().await = Spotify::auth().await; 42 | 43 | // creates the global application commands 44 | self.create_commands(&ctx).await; 45 | 46 | // loads serialized guild settings 47 | self.load_guilds_settings(&ctx, &ready).await; 48 | } 49 | 50 | async fn interaction_create(&self, ctx: Context, interaction: Interaction) { 51 | let Interaction::ApplicationCommand(mut command) = interaction else { 52 | return; 53 | }; 54 | 55 | if let Err(err) = self.run_command(&ctx, &mut command).await { 56 | self.handle_error(&ctx, &mut command, err).await 57 | } 58 | } 59 | 60 | async fn voice_state_update(&self, ctx: Context, _old: Option, new: VoiceState) { 61 | // do nothing if this is a voice update event for a user, not a bot 62 | if new.user_id != ctx.cache.current_user_id() { 63 | return; 64 | } 65 | 66 | if new.channel_id.is_some() { 67 | return self.self_deafen(&ctx, new.guild_id, new).await; 68 | } 69 | 70 | let manager = songbird::get(&ctx).await.unwrap(); 71 | let guild_id = new.guild_id.unwrap(); 72 | 73 | if manager.get(guild_id).is_some() { 74 | manager.remove(guild_id).await.ok(); 75 | } 76 | 77 | update_queue_messages(&ctx.http, &ctx.data, &[], guild_id).await; 78 | } 79 | } 80 | 81 | impl SerenityHandler { 82 | async fn create_commands(&self, ctx: &Context) -> Vec { 83 | Command::set_global_application_commands(&ctx.http, |commands| { 84 | commands 85 | .create_application_command(|command| { 86 | command 87 | .name("autopause") 88 | .description("Toggles whether to pause after a song ends") 89 | }) 90 | .create_application_command(|command| { 91 | command 92 | .name("clear") 93 | .description("Clears the queue") 94 | }) 95 | .create_application_command(|command| { 96 | command 97 | .name("leave") 98 | .description("Leave the voice channel the bot is connected to") 99 | }) 100 | .create_application_command(|command| { 101 | command 102 | .name("managesources") 103 | .description("Manage streaming from different sources") 104 | }) 105 | .create_application_command(|command| { 106 | command 107 | .name("np") 108 | .description("Displays information about the current track") 109 | }) 110 | .create_application_command(|command| { 111 | command 112 | .name("pause") 113 | .description("Pauses the current track") 114 | }) 115 | .create_application_command(|command| { 116 | command 117 | .name("play") 118 | .description("Add a track to the queue") 119 | .create_option(|option| { 120 | option 121 | .name("query") 122 | .description("The media to play") 123 | .kind(CommandOptionType::String) 124 | .required(true) 125 | }) 126 | }) 127 | .create_application_command(|command| { 128 | command 129 | .name("superplay") 130 | .description("Add a track to the queue in a special way") 131 | .create_option(|option| { 132 | option 133 | .name("next") 134 | .description("Add a track to be played up next") 135 | .kind(CommandOptionType::SubCommand) 136 | .create_sub_option(|option| { 137 | option 138 | .name("query") 139 | .description("The media to play") 140 | .kind(CommandOptionType::String) 141 | .required(true) 142 | }) 143 | }) 144 | .create_option(|option| { 145 | option 146 | .name("jump") 147 | .description("Instantly plays a track, skipping the current one") 148 | .kind(CommandOptionType::SubCommand) 149 | .create_sub_option(|option| { 150 | option.name("query") 151 | .description("The media to play") 152 | .kind(CommandOptionType::String) 153 | .required(true) 154 | }) 155 | }) 156 | .create_option(|option| { 157 | option 158 | .name("all") 159 | .description("Add all tracks if the URL refers to a video and a playlist") 160 | .kind(CommandOptionType::SubCommand) 161 | .create_sub_option(|option| { 162 | option 163 | .name("query") 164 | .description("The media to play") 165 | .kind(CommandOptionType::String) 166 | .required(true) 167 | }) 168 | }) 169 | .create_option(|option| { 170 | option 171 | .name("reverse") 172 | .description("Add a playlist to the queue in reverse order") 173 | .kind(CommandOptionType::SubCommand) 174 | .create_sub_option(|option| { 175 | option 176 | .name("query") 177 | .description("The media to play") 178 | .kind(CommandOptionType::String) 179 | .required(true) 180 | }) 181 | }) 182 | .create_option(|option| { 183 | option 184 | .name("shuffle") 185 | .description("Add a playlist to the queue in random order") 186 | .kind(CommandOptionType::SubCommand) 187 | .create_sub_option(|option| { 188 | option 189 | .name("query") 190 | .description("The media to play") 191 | .kind(CommandOptionType::String) 192 | .required(true) 193 | }) 194 | }) 195 | }) 196 | .create_application_command(|command| { 197 | command 198 | .name("queue") 199 | .description("Shows the queue") 200 | }) 201 | .create_application_command(|command| { 202 | command 203 | .name("remove") 204 | .description("Removes a track from the queue") 205 | .create_option(|option| { 206 | option 207 | .name("index") 208 | .description("Position of the track in the queue (1 is the next track to be played)") 209 | .kind(CommandOptionType::Integer) 210 | .required(true) 211 | .min_int_value(1) 212 | }) 213 | .create_option(|option| { 214 | option 215 | .name("until") 216 | .description("Upper range track position to remove a range of tracks") 217 | .kind(CommandOptionType::Integer) 218 | .required(false) 219 | .min_int_value(1) 220 | }) 221 | }) 222 | .create_application_command(|command| { 223 | command 224 | .name("repeat") 225 | .description("Toggles looping for the current track") 226 | }) 227 | .create_application_command(|command| { 228 | command 229 | .name("resume") 230 | .description("Resumes the current track") 231 | }) 232 | .create_application_command(|command| { 233 | command 234 | .name("seek") 235 | .description("Seeks current track to the given position") 236 | .create_option(|option| { 237 | option 238 | .name("timestamp") 239 | .description("Timestamp in the format HH:MM:SS") 240 | .kind(CommandOptionType::String) 241 | .required(true) 242 | }) 243 | }) 244 | .create_application_command(|command| { 245 | command.name("shuffle").description("Shuffles the queue") 246 | }) 247 | .create_application_command(|command| { 248 | command.name("skip").description("Skips the current track") 249 | .create_option(|option| { 250 | option 251 | .name("to") 252 | .description("Track index to skip to") 253 | .kind(CommandOptionType::Integer) 254 | .required(false) 255 | .min_int_value(1) 256 | }) 257 | }) 258 | .create_application_command(|command| { 259 | command 260 | .name("stop") 261 | .description("Stops the bot and clears the queue") 262 | }) 263 | .create_application_command(|command| { 264 | command 265 | .name("summon") 266 | .description("Summons the bot in your voice channel") 267 | }) 268 | .create_application_command(|command| { 269 | command 270 | .name("version") 271 | .description("Displays the current version") 272 | }) 273 | .create_application_command(|command| { 274 | command.name("voteskip").description("Starts a vote to skip the current track") 275 | }) 276 | }) 277 | .await 278 | .expect("failed to create command") 279 | } 280 | 281 | async fn load_guilds_settings(&self, ctx: &Context, ready: &Ready) { 282 | println!("[INFO] Loading guilds' settings"); 283 | let mut data = ctx.data.write().await; 284 | for guild in &ready.guilds { 285 | println!("[DEBUG] Loading guild settings for {:?}", guild); 286 | let settings = data.get_mut::().unwrap(); 287 | 288 | let guild_settings = settings 289 | .entry(guild.id) 290 | .or_insert_with(|| GuildSettings::new(guild.id)); 291 | 292 | if let Err(err) = guild_settings.load_if_exists() { 293 | println!( 294 | "[ERROR] Failed to load guild {} settings due to {}", 295 | guild.id, err 296 | ); 297 | } 298 | } 299 | } 300 | 301 | async fn run_command( 302 | &self, 303 | ctx: &Context, 304 | command: &mut ApplicationCommandInteraction, 305 | ) -> Result<(), ParrotError> { 306 | let command_name = command.data.name.as_str(); 307 | 308 | let guild_id = command.guild_id.unwrap(); 309 | let guild = ctx.cache.guild(guild_id).unwrap(); 310 | 311 | // get songbird voice client 312 | let manager = songbird::get(ctx).await.unwrap(); 313 | 314 | // parrot might have been disconnected manually 315 | if let Some(call) = manager.get(guild.id) { 316 | let mut handler = call.lock().await; 317 | if handler.current_connection().is_none() { 318 | handler.leave().await.unwrap(); 319 | } 320 | } 321 | 322 | // fetch the user and the bot's user IDs 323 | let user_id = command.user.id; 324 | let bot_id = ctx.cache.current_user_id(); 325 | 326 | match command_name { 327 | "autopause" | "clear" | "leave" | "pause" | "remove" | "repeat" | "resume" | "seek" 328 | | "shuffle" | "skip" | "stop" | "voteskip" => { 329 | match check_voice_connections(&guild, &user_id, &bot_id) { 330 | Connection::User(_) | Connection::Neither => Err(ParrotError::NotConnected), 331 | Connection::Bot(bot_channel_id) => { 332 | Err(ParrotError::AuthorDisconnected(bot_channel_id.mention())) 333 | } 334 | Connection::Separate(_, _) => Err(ParrotError::WrongVoiceChannel), 335 | _ => Ok(()), 336 | } 337 | } 338 | "play" | "superplay" | "summon" => { 339 | match check_voice_connections(&guild, &user_id, &bot_id) { 340 | Connection::User(_) => Ok(()), 341 | Connection::Bot(_) if command_name == "summon" => { 342 | Err(ParrotError::AuthorNotFound) 343 | } 344 | Connection::Bot(_) if command_name != "summon" => { 345 | Err(ParrotError::WrongVoiceChannel) 346 | } 347 | Connection::Separate(bot_channel_id, _) => { 348 | Err(ParrotError::AlreadyConnected(bot_channel_id.mention())) 349 | } 350 | Connection::Neither => Err(ParrotError::AuthorNotFound), 351 | _ => Ok(()), 352 | } 353 | } 354 | "np" | "queue" => match check_voice_connections(&guild, &user_id, &bot_id) { 355 | Connection::User(_) | Connection::Neither => Err(ParrotError::NotConnected), 356 | _ => Ok(()), 357 | }, 358 | _ => Ok(()), 359 | }?; 360 | 361 | match command_name { 362 | "autopause" => autopause(ctx, command).await, 363 | "clear" => clear(ctx, command).await, 364 | "leave" => leave(ctx, command).await, 365 | "managesources" => allow(ctx, command).await, 366 | "np" => now_playing(ctx, command).await, 367 | "pause" => pause(ctx, command).await, 368 | "play" | "superplay" => play(ctx, command).await, 369 | "queue" => queue(ctx, command).await, 370 | "remove" => remove(ctx, command).await, 371 | "repeat" => repeat(ctx, command).await, 372 | "resume" => resume(ctx, command).await, 373 | "seek" => seek(ctx, command).await, 374 | "shuffle" => shuffle(ctx, command).await, 375 | "skip" => skip(ctx, command).await, 376 | "stop" => stop(ctx, command).await, 377 | "summon" => summon(ctx, command, true).await, 378 | "version" => version(ctx, command).await, 379 | "voteskip" => voteskip(ctx, command).await, 380 | _ => unreachable!(), 381 | } 382 | } 383 | 384 | async fn self_deafen(&self, ctx: &Context, guild: Option, new: VoiceState) { 385 | let Ok(user) = ctx.http.get_current_user().await else { 386 | return; 387 | }; 388 | 389 | if user.id == new.user_id && !new.deaf { 390 | guild 391 | .unwrap() 392 | .edit_member(&ctx.http, new.user_id, |n| n.deafen(true)) 393 | .await 394 | .unwrap(); 395 | } 396 | } 397 | 398 | async fn handle_error( 399 | &self, 400 | ctx: &Context, 401 | interaction: &mut ApplicationCommandInteraction, 402 | err: ParrotError, 403 | ) { 404 | create_response_text(&ctx.http, interaction, &format!("{err}")) 405 | .await 406 | .expect("failed to create response"); 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /src/handlers/track_end.rs: -------------------------------------------------------------------------------- 1 | use serenity::{ 2 | async_trait, 3 | http::Http, 4 | model::id::GuildId, 5 | prelude::{Mutex, RwLock, TypeMap}, 6 | }; 7 | use songbird::{tracks::TrackHandle, Call, Event, EventContext, EventHandler}; 8 | use std::sync::Arc; 9 | 10 | use crate::{ 11 | commands::{ 12 | queue::{build_nav_btns, calculate_num_pages, create_queue_embed, forget_queue_message}, 13 | voteskip::forget_skip_votes, 14 | }, 15 | guild::{cache::GuildCacheMap, settings::GuildSettingsMap}, 16 | }; 17 | 18 | pub struct TrackEndHandler { 19 | pub guild_id: GuildId, 20 | pub call: Arc>, 21 | pub ctx_data: Arc>, 22 | } 23 | 24 | pub struct ModifyQueueHandler { 25 | pub http: Arc, 26 | pub ctx_data: Arc>, 27 | pub call: Arc>, 28 | pub guild_id: GuildId, 29 | } 30 | 31 | #[async_trait] 32 | impl EventHandler for TrackEndHandler { 33 | async fn act(&self, _ctx: &EventContext<'_>) -> Option { 34 | let data_rlock = self.ctx_data.read().await; 35 | let settings = data_rlock.get::().unwrap(); 36 | 37 | let autopause = settings 38 | .get(&self.guild_id) 39 | .map(|guild_settings| guild_settings.autopause) 40 | .unwrap_or_default(); 41 | 42 | if autopause { 43 | let handler = self.call.lock().await; 44 | let queue = handler.queue(); 45 | queue.pause().ok(); 46 | } 47 | 48 | drop(data_rlock); 49 | forget_skip_votes(&self.ctx_data, self.guild_id).await.ok(); 50 | 51 | None 52 | } 53 | } 54 | 55 | #[async_trait] 56 | impl EventHandler for ModifyQueueHandler { 57 | async fn act(&self, _ctx: &EventContext<'_>) -> Option { 58 | let handler = self.call.lock().await; 59 | let queue = handler.queue().current_queue(); 60 | drop(handler); 61 | 62 | update_queue_messages(&self.http, &self.ctx_data, &queue, self.guild_id).await; 63 | None 64 | } 65 | } 66 | 67 | pub async fn update_queue_messages( 68 | http: &Arc, 69 | ctx_data: &Arc>, 70 | tracks: &[TrackHandle], 71 | guild_id: GuildId, 72 | ) { 73 | let data = ctx_data.read().await; 74 | let cache_map = data.get::().unwrap(); 75 | 76 | let mut messages = match cache_map.get(&guild_id) { 77 | Some(cache) => cache.queue_messages.clone(), 78 | None => return, 79 | }; 80 | drop(data); 81 | 82 | for (message, page_lock) in messages.iter_mut() { 83 | // has the page size shrunk? 84 | let num_pages = calculate_num_pages(tracks); 85 | let mut page = page_lock.write().await; 86 | *page = usize::min(*page, num_pages - 1); 87 | 88 | let embed = create_queue_embed(tracks, *page); 89 | 90 | let edit_message = message 91 | .edit(&http, |edit| { 92 | edit.set_embed(embed); 93 | edit.components(|components| build_nav_btns(components, *page, num_pages)) 94 | }) 95 | .await; 96 | 97 | if edit_message.is_err() { 98 | forget_queue_message(ctx_data, message, guild_id).await.ok(); 99 | }; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod commands; 3 | pub mod connection; 4 | pub mod errors; 5 | pub mod guild; 6 | pub mod handlers; 7 | pub mod messaging; 8 | pub mod sources; 9 | pub mod utils; 10 | 11 | #[cfg(test)] 12 | pub mod test; 13 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use parrot::client::Client; 2 | use std::error::Error; 3 | 4 | #[tokio::main] 5 | async fn main() -> Result<(), Box> { 6 | dotenv::dotenv().ok(); 7 | 8 | let mut parrot = Client::default().await?; 9 | if let Err(why) = parrot.start().await { 10 | println!("Fatality! Parrot crashed because: {:?}", why); 11 | }; 12 | 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /src/messaging/message.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use serenity::model::mention::Mention; 4 | 5 | use crate::messaging::messages::*; 6 | 7 | const RELEASES_LINK: &str = "https://github.com/aquelemiguel/parrot/releases"; 8 | 9 | #[derive(Debug)] 10 | pub enum ParrotMessage { 11 | AutopauseOff, 12 | AutopauseOn, 13 | Clear, 14 | Error, 15 | Leaving, 16 | LoopDisable, 17 | LoopEnable, 18 | NowPlaying, 19 | Pause, 20 | PlayAllFailed, 21 | PlayDomainBanned { domain: String }, 22 | PlaylistQueued, 23 | RemoveMultiple, 24 | Resume, 25 | Search, 26 | Seek { timestamp: String }, 27 | Shuffle, 28 | Skip, 29 | SkipAll, 30 | SkipTo { title: String, url: String }, 31 | Stop, 32 | Summon { mention: Mention }, 33 | Version { current: String }, 34 | VoteSkip { mention: Mention, missing: usize }, 35 | } 36 | 37 | impl Display for ParrotMessage { 38 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 | match self { 40 | Self::AutopauseOff => f.write_str(AUTOPAUSE_OFF), 41 | Self::AutopauseOn => f.write_str(AUTOPAUSE_ON), 42 | Self::Clear => f.write_str(CLEARED), 43 | Self::Error => f.write_str(ERROR), 44 | Self::Leaving => f.write_str(LEAVING), 45 | Self::LoopDisable => f.write_str(LOOP_DISABLED), 46 | Self::LoopEnable => f.write_str(LOOP_ENABLED), 47 | Self::NowPlaying => f.write_str(QUEUE_NOW_PLAYING), 48 | Self::Pause => f.write_str(PAUSED), 49 | Self::PlaylistQueued => f.write_str(PLAY_PLAYLIST), 50 | Self::PlayAllFailed => f.write_str(PLAY_ALL_FAILED), 51 | Self::PlayDomainBanned { domain } => { 52 | f.write_str(&format!("⚠️ **{}** {}", domain, PLAY_FAILED_BLOCKED_DOMAIN)) 53 | } 54 | Self::Search => f.write_str(SEARCHING), 55 | Self::RemoveMultiple => f.write_str(REMOVED_QUEUE_MULTIPLE), 56 | Self::Resume => f.write_str(RESUMED), 57 | Self::Shuffle => f.write_str(SHUFFLED_SUCCESS), 58 | Self::Stop => f.write_str(STOPPED), 59 | Self::VoteSkip { mention, missing } => f.write_str(&format!( 60 | "{}{} {} {} {}", 61 | SKIP_VOTE_EMOJI, mention, SKIP_VOTE_USER, missing, SKIP_VOTE_MISSING 62 | )), 63 | Self::Seek { timestamp } => f.write_str(&format!("{} **{}**!", SEEKED, timestamp)), 64 | Self::Skip => f.write_str(SKIPPED), 65 | Self::SkipAll => f.write_str(SKIPPED_ALL), 66 | Self::SkipTo { title, url } => { 67 | f.write_str(&format!("{} [**{}**]({})!", SKIPPED_TO, title, url)) 68 | } 69 | Self::Summon { mention } => f.write_str(&format!("{} **{}**!", JOINING, mention)), 70 | Self::Version { current } => f.write_str(&format!( 71 | "{} [{}]({}/tag/v{})\n{}({}/latest)", 72 | VERSION, current, RELEASES_LINK, current, VERSION_LATEST, RELEASES_LINK 73 | )), 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/messaging/messages.rs: -------------------------------------------------------------------------------- 1 | pub const AUTOPAUSE_OFF: &str = "🤖 Autopause OFF!"; 2 | pub const AUTOPAUSE_ON: &str = "🤖 Autopause ON!"; 3 | pub const CLEARED: &str = "🗑️ Cleared!"; 4 | 5 | pub const DOMAIN_FORM_ALLOWED_TITLE: &str = "Allowed domains"; 6 | pub const DOMAIN_FORM_BANNED_TITLE: &str = "Banned domains"; 7 | pub const DOMAIN_FORM_ALLOWED_PLACEHOLDER: &str = 8 | "Add domains separated by \';\'. If left blank, all (except for banned) are allowed by default."; 9 | pub const DOMAIN_FORM_BANNED_PLACEHOLDER: &str = 10 | "Add domains separated by \';\'. If left blank, all (except for allowed) are blocked by default."; 11 | pub const DOMAIN_FORM_TITLE: &str = "Manage sources"; 12 | 13 | pub const ERROR: &str = "Fatality! Something went wrong ☹️"; 14 | pub const FAIL_ALREADY_HERE: &str = "⚠️ I'm already here!"; 15 | pub const FAIL_ANOTHER_CHANNEL: &str = "⚠️ I'm already connected to"; 16 | pub const FAIL_AUTHOR_DISCONNECTED: &str = "⚠️ You are not connected to"; 17 | pub const FAIL_AUTHOR_NOT_FOUND: &str = "⚠️ Could not find you in any voice channel!"; 18 | pub const FAIL_LOOP: &str = "⚠️ Failed to toggle loop!"; 19 | pub const FAIL_MINUTES_PARSING: &str = "⚠️ Invalid formatting for 'minutes'"; 20 | pub const FAIL_NO_SONG_ON_INDEX: &str = "⚠️ There is no queued song on that index!"; 21 | pub const FAIL_NO_VOICE_CONNECTION: &str = "⚠️ I'm not connected to any voice channel!"; 22 | pub const FAIL_REMOVE_RANGE: &str = "⚠️ `until` needs to be higher than `index`!"; 23 | pub const FAIL_SECONDS_PARSING: &str = "⚠️ Invalid formatting for 'seconds'"; 24 | pub const FAIL_WRONG_CHANNEL: &str = "⚠️ We are not in the same voice channel!"; 25 | pub const IDLE_ALERT: &str = "I've been idle for a while, so I'll leave for now to save resources.\nFeel free to summon me back any time!"; 26 | pub const JOINING: &str = "Joining"; 27 | pub const LEAVING: &str = "👋 See you soon!"; 28 | pub const LOOP_DISABLED: &str = "🔁 Disabled loop!"; 29 | pub const LOOP_ENABLED: &str = "🔁 Enabled loop!"; 30 | pub const NOTHING_IS_PLAYING: &str = "🔈 Nothing is playing!"; 31 | pub const PAUSED: &str = "⏸️ Paused!"; 32 | pub const PLAY_FAILED_BLOCKED_DOMAIN: &str = 33 | "**is either not allowed in this server or is not supported!** \n\nTo explicitely allow this domain, ask a moderator to run the `/managesources` command. [Click to see a list of supported sources.](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md)"; 34 | pub const PLAY_ALL_FAILED: &str = 35 | "⚠️ Cannot fetch playlist via keywords! Try passing this command an URL."; 36 | pub const PLAY_PLAYLIST: &str = "📃 Added playlist to queue!"; 37 | pub const PLAY_QUEUE: &str = "📃 Added to queue!"; 38 | pub const PLAY_TOP: &str = "📃 Added to top!"; 39 | pub const QUEUE_EXPIRED: &str = 40 | "In order to save resources, this command has expired.\nPlease feel free to reinvoke it!"; 41 | pub const QUEUE_IS_EMPTY: &str = "Queue is empty!"; 42 | pub const QUEUE_NO_SONGS: &str = "There's no songs up next!"; 43 | pub const QUEUE_NOTHING_IS_PLAYING: &str = "Nothing is playing!"; 44 | pub const QUEUE_NOW_PLAYING: &str = "🔊 Now playing"; 45 | pub const QUEUE_PAGE_OF: &str = "of"; 46 | pub const QUEUE_PAGE: &str = "Page"; 47 | pub const QUEUE_UP_NEXT: &str = "⌛ Up next"; 48 | pub const REMOVED_QUEUE_MULTIPLE: &str = "❌ Removed multiple tracks from queue!"; 49 | pub const REMOVED_QUEUE: &str = "❌ Removed from queue"; 50 | pub const RESUMED: &str = "▶️ Resumed!"; 51 | pub const SEARCHING: &str = "🔎 Searching..."; 52 | pub const SEEKED: &str = "⏩ Seeked current track to"; 53 | pub const SHUFFLED_SUCCESS: &str = "🔀 Shuffled successfully!"; 54 | pub const SKIP_VOTE_EMOJI: &str = "🗳 "; 55 | pub const SKIP_VOTE_MISSING: &str = "more vote(s) needed to skip!"; 56 | pub const SKIP_VOTE_USER: &str = "has voted to skip!"; 57 | pub const SKIPPED_ALL: &str = "⏭️ Skipped until infinity!"; 58 | pub const SKIPPED_TO: &str = "⏭️ Skipped to"; 59 | pub const SKIPPED: &str = "⏭️ Skipped!"; 60 | pub const SPOTIFY_AUTH_FAILED: &str = "⚠️ **Could not authenticate with Spotify!**\nDid you forget to provide your Spotify application's client ID and secret?"; 61 | pub const SPOTIFY_INVALID_QUERY: &str = 62 | "⚠️ **Could not find any tracks with that link!**\nAre you sure that is a valid Spotify URL?"; 63 | pub const SPOTIFY_PLAYLIST_FAILED: &str = "⚠️ **Failed to fetch playlist!**\nIt's likely that this playlist is either private or a personalized recommendation playlist generated by Spotify."; 64 | pub const STOPPED: &str = "⏹️ Stopped!"; 65 | pub const TRACK_DURATION: &str = "Track duration: "; 66 | pub const TRACK_NOT_FOUND: &str = "⚠️ **Could not play track!**\nYour request yielded no results."; 67 | pub const TRACK_INAPPROPRIATE: &str = "⚠️ **Could not play track!**\nThe video you requested may be inappropriate for some users, so sign-in is required."; 68 | pub const TRACK_TIME_TO_PLAY: &str = "Estimated time until play: "; 69 | pub const VERSION_LATEST: &str = "Find the latest version [here]"; 70 | pub const VERSION: &str = "Version"; 71 | -------------------------------------------------------------------------------- /src/messaging/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod message; 2 | pub mod messages; 3 | -------------------------------------------------------------------------------- /src/sources/ffmpeg.rs: -------------------------------------------------------------------------------- 1 | use songbird::input::{ 2 | error::{Error, Result}, 3 | Codec, Container, Input, Metadata, Reader, 4 | }; 5 | use std::process::{Child, Command, Stdio}; 6 | 7 | pub async fn ffmpeg(mut source: Child, metadata: Metadata, pre_args: &[&str]) -> Result { 8 | let ffmpeg_args = [ 9 | "-i", 10 | "-", // read from stdout 11 | "-f", 12 | "s16le", // use PCM signed 16-bit little-endian format 13 | "-ac", 14 | "2", // set two audio channels 15 | "-ar", 16 | "48000", // set audio sample rate of 48000Hz 17 | "-acodec", 18 | "pcm_f32le", 19 | "-", 20 | ]; 21 | 22 | let taken_stdout = source.stdout.take().ok_or(Error::Stdout)?; 23 | 24 | let ffmpeg = Command::new("ffmpeg") 25 | .args(pre_args) 26 | .args(ffmpeg_args) 27 | .stdin(taken_stdout) 28 | .stderr(Stdio::null()) 29 | .stdout(Stdio::piped()) 30 | .spawn()?; 31 | 32 | let reader = Reader::from(vec![source, ffmpeg]); 33 | 34 | let input = Input::new( 35 | true, 36 | reader, 37 | Codec::FloatPcm, 38 | Container::Raw, 39 | Some(metadata), 40 | ); 41 | 42 | Ok(input) 43 | } 44 | -------------------------------------------------------------------------------- /src/sources/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ffmpeg; 2 | pub mod spotify; 3 | pub mod youtube; 4 | -------------------------------------------------------------------------------- /src/sources/spotify.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | commands::play::QueryType, 3 | errors::ParrotError, 4 | messaging::messages::{SPOTIFY_INVALID_QUERY, SPOTIFY_PLAYLIST_FAILED}, 5 | }; 6 | use lazy_static::lazy_static; 7 | use regex::Regex; 8 | use rspotify::{ 9 | clients::BaseClient, 10 | model::{AlbumId, PlayableItem, PlaylistId, SimplifiedArtist, TrackId}, 11 | ClientCredsSpotify, Credentials, 12 | }; 13 | use std::{env, str::FromStr}; 14 | use tokio::sync::Mutex; 15 | 16 | lazy_static! { 17 | pub static ref SPOTIFY: Mutex> = 18 | Mutex::new(Err(ParrotError::Other("no auth attempts"))); 19 | pub static ref SPOTIFY_QUERY_REGEX: Regex = 20 | Regex::new(r"spotify.com/(?P.+)/(?P.*?)(?:\?|$)").unwrap(); 21 | } 22 | 23 | #[derive(Clone, Copy)] 24 | pub enum MediaType { 25 | Track, 26 | Album, 27 | Playlist, 28 | } 29 | 30 | impl FromStr for MediaType { 31 | type Err = (); 32 | 33 | fn from_str(s: &str) -> Result { 34 | match s { 35 | "track" => Ok(Self::Track), 36 | "album" => Ok(Self::Album), 37 | "playlist" => Ok(Self::Playlist), 38 | _ => Err(()), 39 | } 40 | } 41 | } 42 | 43 | pub struct Spotify {} 44 | 45 | impl Spotify { 46 | pub async fn auth() -> Result { 47 | let spotify_client_id = env::var("SPOTIFY_CLIENT_ID") 48 | .map_err(|_| ParrotError::Other("missing spotify client ID"))?; 49 | 50 | let spotify_client_secret = env::var("SPOTIFY_CLIENT_SECRET") 51 | .map_err(|_| ParrotError::Other("missing spotify client secret"))?; 52 | 53 | let creds = Credentials::new(&spotify_client_id, &spotify_client_secret); 54 | 55 | let spotify = ClientCredsSpotify::new(creds); 56 | spotify.request_token().await?; 57 | 58 | Ok(spotify) 59 | } 60 | 61 | pub async fn extract( 62 | spotify: &ClientCredsSpotify, 63 | query: &str, 64 | ) -> Result { 65 | let captures = SPOTIFY_QUERY_REGEX 66 | .captures(query) 67 | .ok_or(ParrotError::Other(SPOTIFY_INVALID_QUERY))?; 68 | 69 | let media_type = captures 70 | .name("media_type") 71 | .ok_or(ParrotError::Other(SPOTIFY_INVALID_QUERY))? 72 | .as_str(); 73 | 74 | let media_type = MediaType::from_str(media_type) 75 | .map_err(|_| ParrotError::Other(SPOTIFY_INVALID_QUERY))?; 76 | 77 | let media_id = captures 78 | .name("media_id") 79 | .ok_or(ParrotError::Other(SPOTIFY_INVALID_QUERY))? 80 | .as_str(); 81 | 82 | match media_type { 83 | MediaType::Track => Self::get_track_info(spotify, media_id).await, 84 | MediaType::Album => Self::get_album_info(spotify, media_id).await, 85 | MediaType::Playlist => Self::get_playlist_info(spotify, media_id).await, 86 | } 87 | } 88 | 89 | async fn get_track_info( 90 | spotify: &ClientCredsSpotify, 91 | id: &str, 92 | ) -> Result { 93 | let track_id = TrackId::from_id(id) 94 | .map_err(|_| ParrotError::Other("track ID contains invalid characters"))?; 95 | 96 | let track = spotify 97 | .track(track_id, None) 98 | .await 99 | .map_err(|_| ParrotError::Other("failed to fetch track"))?; 100 | 101 | let artist_names = Self::join_artist_names(&track.artists); 102 | 103 | let query = Self::build_query(&artist_names, &track.name); 104 | Ok(QueryType::Keywords(query)) 105 | } 106 | 107 | async fn get_album_info( 108 | spotify: &ClientCredsSpotify, 109 | id: &str, 110 | ) -> Result { 111 | let album_id = AlbumId::from_id(id) 112 | .map_err(|_| ParrotError::Other("album ID contains invalid characters"))?; 113 | 114 | let album = spotify 115 | .album(album_id, None) 116 | .await 117 | .map_err(|_| ParrotError::Other("failed to fetch album"))?; 118 | 119 | let artist_names = Self::join_artist_names(&album.artists); 120 | 121 | let query_list: Vec = album 122 | .tracks 123 | .items 124 | .iter() 125 | .map(|track| Self::build_query(&artist_names, &track.name)) 126 | .collect(); 127 | 128 | Ok(QueryType::KeywordList(query_list)) 129 | } 130 | 131 | async fn get_playlist_info( 132 | spotify: &ClientCredsSpotify, 133 | id: &str, 134 | ) -> Result { 135 | let playlist_id = PlaylistId::from_id(id) 136 | .map_err(|_| ParrotError::Other("playlist ID contains invalid characters"))?; 137 | 138 | let playlist = spotify 139 | .playlist(playlist_id, None, None) 140 | .await 141 | .map_err(|_| ParrotError::Other(SPOTIFY_PLAYLIST_FAILED))?; 142 | 143 | let query_list: Vec = playlist 144 | .tracks 145 | .items 146 | .iter() 147 | .filter_map(|item| match item.track.as_ref().unwrap() { 148 | PlayableItem::Track(track) => { 149 | let artist_names = Self::join_artist_names(&track.album.artists); 150 | Some(Self::build_query(&artist_names, &track.name)) 151 | } 152 | PlayableItem::Episode(_) => None, 153 | }) 154 | .collect(); 155 | 156 | Ok(QueryType::KeywordList(query_list)) 157 | } 158 | 159 | fn build_query(artists: &str, track_name: &str) -> String { 160 | format!("{} - {}", artists, track_name) 161 | } 162 | 163 | fn join_artist_names(artists: &[SimplifiedArtist]) -> String { 164 | let artist_names: Vec = artists.iter().map(|artist| artist.name.clone()).collect(); 165 | artist_names.join(" ") 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/sources/youtube.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | commands::play::{Mode, QueryType}, 3 | sources::ffmpeg::ffmpeg, 4 | }; 5 | use serde_json::Value; 6 | use serenity::async_trait; 7 | use songbird::input::{ 8 | error::{Error as SongbirdError, Result as SongbirdResult}, 9 | restartable::Restart, 10 | Codec, Container, Input, Metadata, Restartable, 11 | }; 12 | use std::{ 13 | io::{BufRead, BufReader, Read}, 14 | process::Command, 15 | process::{Child, Stdio}, 16 | time::Duration, 17 | }; 18 | use tokio::{process::Command as TokioCommand, task}; 19 | 20 | const NEWLINE_BYTE: u8 = 0xA; 21 | 22 | pub struct YouTube {} 23 | 24 | impl YouTube { 25 | pub fn extract(query: &str) -> Option { 26 | if query.contains("list=") { 27 | Some(QueryType::PlaylistLink(query.to_string())) 28 | } else { 29 | Some(QueryType::VideoLink(query.to_string())) 30 | } 31 | } 32 | } 33 | 34 | pub struct YouTubeRestartable {} 35 | 36 | impl YouTubeRestartable { 37 | pub async fn ytdl + Send + Clone + Sync + 'static>( 38 | uri: P, 39 | lazy: bool, 40 | ) -> SongbirdResult { 41 | Restartable::new(YouTubeRestarter { uri }, lazy).await 42 | } 43 | 44 | pub async fn ytdl_search + Send + Clone + Sync + 'static>( 45 | uri: P, 46 | lazy: bool, 47 | ) -> SongbirdResult { 48 | let uri = format!("ytsearch:{}", uri.as_ref()); 49 | Restartable::new(YouTubeRestarter { uri }, lazy).await 50 | } 51 | 52 | pub async fn ytdl_playlist(uri: &str, mode: Mode) -> Option> { 53 | let mut args = vec![uri, "--flat-playlist", "-j"]; 54 | match mode { 55 | Mode::Reverse => args.push("--playlist-reverse"), 56 | Mode::Shuffle => args.push("--playlist-random"), 57 | _ => {} 58 | } 59 | 60 | let mut child = Command::new("yt-dlp") 61 | .args(args) 62 | .stdout(Stdio::piped()) 63 | .spawn() 64 | .unwrap(); 65 | 66 | let Some(stdout) = &mut child.stdout else { 67 | return None; 68 | }; 69 | 70 | let reader = BufReader::new(stdout); 71 | 72 | let lines = reader.lines().map_while(Result::ok).map(|line| { 73 | let entry: Value = serde_json::from_str(&line).unwrap(); 74 | entry 75 | .get("webpage_url") 76 | .unwrap() 77 | .as_str() 78 | .unwrap() 79 | .to_string() 80 | }); 81 | 82 | Some(lines.collect()) 83 | } 84 | } 85 | 86 | struct YouTubeRestarter

87 | where 88 | P: AsRef + Send + Sync, 89 | { 90 | uri: P, 91 | } 92 | 93 | #[async_trait] 94 | impl

Restart for YouTubeRestarter

95 | where 96 | P: AsRef + Send + Clone + Sync, 97 | { 98 | async fn call_restart(&mut self, time: Option) -> SongbirdResult { 99 | let (yt, metadata) = ytdl(self.uri.as_ref()).await?; 100 | 101 | let Some(time) = time else { 102 | return ffmpeg(yt, metadata, &[]).await; 103 | }; 104 | 105 | let ts = format!("{:.3}", time.as_secs_f64()); 106 | ffmpeg(yt, metadata, &["-ss", &ts]).await 107 | } 108 | 109 | async fn lazy_init(&mut self) -> SongbirdResult<(Option, Codec, Container)> { 110 | _ytdl_metadata(self.uri.as_ref()) 111 | .await 112 | .map(|m| (Some(m), Codec::FloatPcm, Container::Raw)) 113 | } 114 | } 115 | 116 | async fn ytdl(uri: &str) -> Result<(Child, Metadata), SongbirdError> { 117 | let ytdl_args = [ 118 | "-j", // print JSON information for video for metadata 119 | "-q", // don't print progress logs (this messes with -o -) 120 | "--no-simulate", // ensure video is downloaded regardless of printing 121 | "-f", 122 | "webm[abr>0]/bestaudio/best", // select best quality audio-only 123 | "-R", 124 | "infinite", // infinite number of download retries 125 | "--no-playlist", // only download the video if URL also has playlist info 126 | "--ignore-config", // disable all configuration files for a yt-dlp run 127 | "--no-warnings", // don't print out warnings 128 | uri, 129 | "-o", 130 | "-", // stream data to stdout 131 | ]; 132 | 133 | let mut yt = Command::new("yt-dlp") 134 | .args(ytdl_args) 135 | .stdin(Stdio::null()) 136 | .stderr(Stdio::piped()) 137 | .stdout(Stdio::piped()) 138 | .spawn()?; 139 | 140 | // track info json (for metadata) is piped to stderr by design choice of yt-dlp 141 | // the actual track is streamed to stdout 142 | let stderr = yt.stderr.take(); 143 | let (returned_stderr, value) = task::spawn_blocking(move || { 144 | let mut s = stderr.unwrap(); 145 | let out: SongbirdResult = { 146 | let mut o_vec = vec![]; 147 | let mut serde_read = BufReader::new(s.by_ref()); 148 | 149 | if let Ok(len) = serde_read.read_until(NEWLINE_BYTE, &mut o_vec) { 150 | serde_json::from_slice(&o_vec[..len]).map_err(|err| SongbirdError::Json { 151 | error: err, 152 | parsed_text: std::str::from_utf8(&o_vec).unwrap_or_default().to_string(), 153 | }) 154 | } else { 155 | Result::Err(SongbirdError::Metadata) 156 | } 157 | }; 158 | 159 | (s, out) 160 | }) 161 | .await 162 | .map_err(|_| SongbirdError::Metadata)?; 163 | 164 | let metadata = Metadata::from_ytdl_output(value?); 165 | yt.stderr = Some(returned_stderr); 166 | 167 | Ok((yt, metadata)) 168 | } 169 | 170 | async fn _ytdl_metadata(uri: &str) -> SongbirdResult { 171 | let ytdl_args = [ 172 | "-j", // print JSON information for video for metadata 173 | "-R", 174 | "infinite", // infinite number of download retries 175 | "--no-playlist", // only download the video if URL also has playlist info 176 | "--ignore-config", // disable all configuration files for a yt-dlp run 177 | "--no-warnings", // don't print out warnings 178 | uri, 179 | "-o", 180 | "-", // stream data to stdout 181 | ]; 182 | 183 | let youtube_dl_output = TokioCommand::new("yt-dlp") 184 | .args(ytdl_args) 185 | .stdin(Stdio::null()) 186 | .output() 187 | .await?; 188 | 189 | let o_vec = youtube_dl_output.stderr; 190 | 191 | // read until newline byte 192 | let end = (o_vec) 193 | .iter() 194 | .position(|el| *el == NEWLINE_BYTE) 195 | .unwrap_or(o_vec.len()); 196 | 197 | let value = serde_json::from_slice(&o_vec[..end]).map_err(|err| SongbirdError::Json { 198 | error: err, 199 | parsed_text: std::str::from_utf8(&o_vec).unwrap_or_default().to_string(), 200 | })?; 201 | 202 | let metadata = Metadata::from_ytdl_output(value); 203 | Ok(metadata) 204 | } 205 | -------------------------------------------------------------------------------- /src/test/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::{verify, ParrotError}; 2 | 3 | #[test] 4 | fn test_verify_bools() { 5 | let x = true; 6 | let x = verify(x, ParrotError::Other("not true")); 7 | assert_eq!(x, Ok(true)); 8 | 9 | let x = false; 10 | let x = verify(x, ParrotError::Other("not true")); 11 | assert_eq!(x, Err(ParrotError::Other("not true"))); 12 | } 13 | 14 | #[test] 15 | fn test_verify_options() { 16 | let x = Some("🦜"); 17 | let x = verify(x, ParrotError::Other("not something")); 18 | assert_eq!(x, Ok("🦜")); 19 | 20 | let x: Option<&str> = None; 21 | let x = verify(x, ParrotError::Other("not something")); 22 | assert_eq!(x, Err(ParrotError::Other("not something"))); 23 | } 24 | 25 | #[test] 26 | fn test_verify_results() { 27 | let x: Result<&str, &str> = Ok("🦜"); 28 | let x = verify(x, ParrotError::Other("not ok")); 29 | assert_eq!(x, Ok("🦜")); 30 | 31 | let x: Result<&str, &str> = Err("fatality"); 32 | let x = verify(x, ParrotError::Other("not ok")); 33 | assert_eq!(x, Err(ParrotError::Other("not ok"))); 34 | } 35 | -------------------------------------------------------------------------------- /src/test/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod errors; 2 | pub mod utils; 3 | -------------------------------------------------------------------------------- /src/test/utils.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::utils::get_human_readable_timestamp; 4 | 5 | #[test] 6 | fn test_get_human_readable_timestamp() { 7 | let duration = Duration::from_secs(53); 8 | let result = get_human_readable_timestamp(Some(duration)); 9 | assert_eq!(result, "00:53"); 10 | 11 | let duration = Duration::from_secs(3599); 12 | let result = get_human_readable_timestamp(Some(duration)); 13 | assert_eq!(result, "59:59"); 14 | 15 | let duration = Duration::from_secs(96548); 16 | let result = get_human_readable_timestamp(Some(duration)); 17 | assert_eq!(result, "26:49:08"); 18 | 19 | let result = get_human_readable_timestamp(Some(Duration::MAX)); 20 | assert_eq!(result, "∞"); 21 | 22 | let result = get_human_readable_timestamp(None); 23 | assert_eq!(result, "∞"); 24 | } 25 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use serenity::{ 2 | builder::CreateEmbed, 3 | http::{Http, HttpError}, 4 | model::{ 5 | application::interaction::{ 6 | application_command::ApplicationCommandInteraction, InteractionResponseType, 7 | }, 8 | channel::Message, 9 | }, 10 | Error, 11 | }; 12 | use songbird::tracks::TrackHandle; 13 | use std::{sync::Arc, time::Duration}; 14 | use url::Url; 15 | 16 | use crate::{errors::ParrotError, messaging::message::ParrotMessage}; 17 | 18 | pub async fn create_response( 19 | http: &Arc, 20 | interaction: &mut ApplicationCommandInteraction, 21 | message: ParrotMessage, 22 | ) -> Result<(), ParrotError> { 23 | let mut embed = CreateEmbed::default(); 24 | embed.description(format!("{message}")); 25 | create_embed_response(http, interaction, embed).await 26 | } 27 | 28 | pub async fn create_response_text( 29 | http: &Arc, 30 | interaction: &mut ApplicationCommandInteraction, 31 | content: &str, 32 | ) -> Result<(), ParrotError> { 33 | let mut embed = CreateEmbed::default(); 34 | embed.description(content); 35 | create_embed_response(http, interaction, embed).await 36 | } 37 | 38 | pub async fn edit_response( 39 | http: &Arc, 40 | interaction: &mut ApplicationCommandInteraction, 41 | message: ParrotMessage, 42 | ) -> Result { 43 | let mut embed = CreateEmbed::default(); 44 | embed.description(format!("{message}")); 45 | edit_embed_response(http, interaction, embed).await 46 | } 47 | 48 | pub async fn edit_response_text( 49 | http: &Arc, 50 | interaction: &mut ApplicationCommandInteraction, 51 | content: &str, 52 | ) -> Result { 53 | let mut embed = CreateEmbed::default(); 54 | embed.description(content); 55 | edit_embed_response(http, interaction, embed).await 56 | } 57 | 58 | pub async fn create_embed_response( 59 | http: &Arc, 60 | interaction: &mut ApplicationCommandInteraction, 61 | embed: CreateEmbed, 62 | ) -> Result<(), ParrotError> { 63 | match interaction 64 | .create_interaction_response(&http, |response| { 65 | response 66 | .kind(InteractionResponseType::ChannelMessageWithSource) 67 | .interaction_response_data(|message| message.add_embed(embed.clone())) 68 | }) 69 | .await 70 | .map_err(Into::into) 71 | { 72 | Ok(val) => Ok(val), 73 | Err(err) => match err { 74 | ParrotError::Serenity(Error::Http(ref e)) => match &**e { 75 | HttpError::UnsuccessfulRequest(req) => match req.error.code { 76 | 40060 => edit_embed_response(http, interaction, embed) 77 | .await 78 | .map(|_| ()), 79 | _ => Err(err), 80 | }, 81 | _ => Err(err), 82 | }, 83 | _ => Err(err), 84 | }, 85 | } 86 | } 87 | 88 | pub async fn edit_embed_response( 89 | http: &Arc, 90 | interaction: &mut ApplicationCommandInteraction, 91 | embed: CreateEmbed, 92 | ) -> Result { 93 | interaction 94 | .edit_original_interaction_response(&http, |message| message.content(" ").add_embed(embed)) 95 | .await 96 | .map_err(Into::into) 97 | } 98 | 99 | pub async fn create_now_playing_embed(track: &TrackHandle) -> CreateEmbed { 100 | let mut embed = CreateEmbed::default(); 101 | let metadata = track.metadata().clone(); 102 | 103 | embed.author(|author| author.name(ParrotMessage::NowPlaying)); 104 | embed.title(metadata.title.unwrap()); 105 | embed.url(metadata.source_url.as_ref().unwrap()); 106 | 107 | let position = get_human_readable_timestamp(Some(track.get_info().await.unwrap().position)); 108 | let duration = get_human_readable_timestamp(metadata.duration); 109 | 110 | embed.field("Progress", format!(">>> {} / {}", position, duration), true); 111 | 112 | match metadata.channel { 113 | Some(channel) => embed.field("Channel", format!(">>> {}", channel), true), 114 | None => embed.field("Channel", ">>> N/A", true), 115 | }; 116 | 117 | embed.thumbnail(&metadata.thumbnail.unwrap()); 118 | 119 | let source_url = metadata.source_url.as_ref().unwrap(); 120 | 121 | let (footer_text, footer_icon_url) = get_footer_info(source_url); 122 | embed.footer(|f| f.text(footer_text).icon_url(footer_icon_url)); 123 | 124 | embed 125 | } 126 | 127 | pub fn get_footer_info(url: &str) -> (String, String) { 128 | let url_data = Url::parse(url).unwrap(); 129 | let domain = url_data.host_str().unwrap(); 130 | 131 | // remove www prefix because it looks ugly 132 | let domain = domain.replace("www.", ""); 133 | 134 | ( 135 | format!("Streaming via {}", domain), 136 | format!("https://www.google.com/s2/favicons?domain={}", domain), 137 | ) 138 | } 139 | 140 | pub fn get_human_readable_timestamp(duration: Option) -> String { 141 | match duration { 142 | Some(duration) if duration == Duration::MAX => "∞".to_string(), 143 | Some(duration) => { 144 | let seconds = duration.as_secs() % 60; 145 | let minutes = (duration.as_secs() / 60) % 60; 146 | let hours = duration.as_secs() / 3600; 147 | 148 | if hours < 1 { 149 | format!("{:02}:{:02}", minutes, seconds) 150 | } else { 151 | format!("{}:{:02}:{:02}", hours, minutes, seconds) 152 | } 153 | } 154 | None => "∞".to_string(), 155 | } 156 | } 157 | 158 | pub fn compare_domains(domain: &str, subdomain: &str) -> bool { 159 | subdomain == domain || subdomain.ends_with(domain) 160 | } 161 | --------------------------------------------------------------------------------